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

# Directory Structure

```
├── .clinerules
├── .env.example
├── .eslintrc.json
├── .github
│   ├── copilot-instructions.md
│   ├── FUNDING.yml
│   ├── release-please-config.json
│   ├── release-please-manifest.json
│   ├── skills
│   │   ├── azure-devops-rest-api
│   │   │   ├── references
│   │   │   │   └── api_areas.md
│   │   │   ├── scripts
│   │   │   │   ├── clone_specs.sh
│   │   │   │   └── find_endpoint.py
│   │   │   └── SKILL.md
│   │   └── skill-creator
│   │       ├── LICENSE.txt
│   │       ├── references
│   │       │   ├── output-patterns.md
│   │       │   └── workflows.md
│   │       ├── scripts
│   │       │   ├── init_skill.py
│   │       │   └── quick_validate.py
│   │       └── SKILL.md
│   └── workflows
│       ├── main.yml
│       ├── release-please.yml
│       └── update-skills.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
│   │   │   ├── artifacts.spec.unit.ts
│   │   │   ├── artifacts.ts
│   │   │   ├── download-pipeline-artifact
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-pipeline
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-pipeline-log
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── get-pipeline-run
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── helpers.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-pipeline-runs
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── list-pipelines
│   │   │   │   ├── feature.spec.int.ts
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── schema.ts
│   │   │   ├── pipeline-timeline
│   │   │   │   ├── 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-changes
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.ts
│   │   │   ├── get-pull-request-checks
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.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
│   │   │   ├── create-branch
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.ts
│   │   │   ├── create-commit
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.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
│   │   │   ├── get-repository-tree
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.ts
│   │   │   │   └── index.ts
│   │   │   ├── index.spec.unit.ts
│   │   │   ├── index.ts
│   │   │   ├── list-commits
│   │   │   │   ├── feature.spec.unit.ts
│   │   │   │   ├── feature.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
│   ├── types
│   │   └── diff.d.ts
│   └── utils
│       ├── environment.spec.unit.ts
│       └── environment.ts
├── tasks.json
├── tests
│   └── setup.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.github/skills/azure-devops-rest-api/SKILL.md:
--------------------------------------------------------------------------------

```markdown
---
name: azure-devops-rest-api
description: Guide for working with Azure DevOps REST APIs and OpenAPI specifications. Use this skill when implementing new Azure DevOps API integrations, exploring API capabilities, understanding request/response formats, or referencing the official OpenAPI specifications from the vsts-rest-api-specs repository.
---

# Azure DevOps REST API

## Overview

This skill provides guidance for working with Azure DevOps REST APIs using the official OpenAPI specifications from the [vsts-rest-api-specs](https://github.com/MicrosoftDocs/vsts-rest-api-specs) repository. It helps with implementing new API integrations, understanding API capabilities, and referencing the correct request/response formats.

## Key API Areas

Azure DevOps REST APIs are organized into the following main areas:

### Core Services
- **core** - Projects, teams, processes, and organization-level operations
- **git** - Repositories, branches, pull requests, commits
- **build** - Build definitions, builds, and build resources
- **pipelines** - Pipeline definitions and runs
- **release** - Release definitions, deployments, and approvals

### Work Item & Planning
- **wit** (Work Item Tracking) - Work items, queries, and work item types
- **work** - Boards, backlogs, sprints, and team configurations
- **testPlan** - Test plans, suites, and cases
- **testResults** - Test runs and results

### Package & Artifact Management
- **artifacts** - Artifact feeds and packages
- **artifactsPackageTypes** - NuGet, npm, Maven, Python packages

### Security & Governance
- **graph** - Users, groups, and memberships
- **security** - Access control lists and permissions
- **policy** - Branch policies and policy configurations
- **audit** - Audit logs and events

### Extension & Integration
- **extensionManagement** - Extensions and marketplace
- **serviceEndpoint** - Service connections
- **hooks** - Service hooks and subscriptions

## API Specification Structure

The vsts-rest-api-specs repository is organized by API area and version:

```
specification/
  ├── {api-area}/          (e.g., git, build, pipelines)
  │   ├── 7.2/            Latest stable version
  │   │   ├── {area}.json          OpenAPI spec file
  │   │   └── httpExamples/        Example requests/responses
  │   ├── 7.1/
  │   └── ...
```

### Using the Specifications

1. **Identify the API area** - Determine which service area (git, build, wit, etc.) contains the functionality needed
2. **Select the version** - Use the latest version (7.2) unless targeting a specific Azure DevOps Server version
3. **Review the OpenAPI spec** - The main `{area}.json` file contains all endpoints, schemas, and parameters
4. **Check httpExamples** - Real request/response examples for each endpoint

## Implementation Patterns

### Pattern 1: Exploring New API Capabilities

When exploring what APIs are available for a specific feature:

1. Clone the vsts-rest-api-specs repository to `/tmp/vsts-rest-api-specs`
2. Browse the specification directory for the relevant API area
3. Review the OpenAPI spec JSON file for available endpoints
4. Check httpExamples for real-world usage patterns

Example workflow:
```bash
cd /tmp
git clone --depth 1 https://github.com/MicrosoftDocs/vsts-rest-api-specs.git
cd vsts-rest-api-specs/specification/git/7.2
# Review git.json for endpoint definitions
# Check httpExamples/ for request/response samples
```

### Pattern 2: Implementing a New API Feature

When adding new API functionality to the MCP server:

1. **Locate the spec**: Find the relevant OpenAPI specification
2. **Review the schema**: Understand the request parameters and response format
3. **Check examples**: Review httpExamples for real request/response data
4. **Create types**: Define TypeScript interfaces based on the OpenAPI schema
5. **Implement handler**: Create the feature handler following the repository's feature-based architecture
6. **Add tests**: Write unit tests with mocked responses based on httpExamples

### Pattern 3: Understanding Request/Response Formats

When you need to understand the exact format of API requests or responses:

1. Navigate to `specification/{area}/{version}/httpExamples/`
2. Find the relevant endpoint example (e.g., `GET_repositories.json`)
3. Review both the request parameters and response body structure
4. Use this as the basis for creating Zod schemas and TypeScript types

## Reference Resources

### scripts/
Contains helper utilities for working with the API specifications:
- `clone_specs.sh` - Clone or update the vsts-rest-api-specs repository
- `find_endpoint.py` - Search for specific endpoints across all API specs

### references/
Contains curated reference documentation:
- `api_areas.md` - Comprehensive list of all API areas and their purposes
- `common_patterns.md` - Common request/response patterns across APIs
- `authentication.md` - API authentication methods and patterns

### Usage Tips

**Finding the right API:**
- Use the OpenAPI spec's `paths` section to find endpoint URLs
- Check the `tags` property to understand the API category
- Review the `operationId` for the internal Azure DevOps method name

**Understanding schemas:**
- All data models are in the `definitions` section of the OpenAPI spec
- Look for `$ref` properties to follow schema references
- Use httpExamples to see actual data structures

**Version selection:**
- Use version 7.2 for Azure DevOps Services (cloud)
- Check azure-devops-server-{version} folders for on-premises versions
- API versions are additive - newer versions include all older functionality

## Integration with This Repository

When implementing features in this MCP server:

1. **Follow the feature-based architecture**: Create features in `src/features/{api-area}/`
2. **Use Zod for validation**: Define schemas based on OpenAPI definitions
3. **Reference the specs**: Link to the relevant OpenAPI spec in code comments
4. **Include examples**: Use httpExamples data for test fixtures
5. **Match naming**: Use consistent naming with the Azure DevOps API (e.g., `repositoryId`, `pullRequestId`)

Example feature implementation pattern:
```typescript
// src/features/{api-area}/{operation}/
├── feature.ts          // Core implementation using azure-devops-node-api
├── schema.ts           // Zod schemas based on OpenAPI definitions
├── feature.spec.unit.ts // Tests using httpExamples data
└── index.ts            // Exports
```

```

--------------------------------------------------------------------------------
/src/features/wikis/create-wiki-page/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { createWikiPage } from './feature';
import { handleRequestError } from '../../../shared/errors/handle-request-error';

// Mock the AzureDevOpsClient
jest.mock('../../../shared/api/client');
// Mock the error handler
jest.mock('../../../shared/errors/handle-request-error', () => ({
  handleRequestError: jest.fn(),
}));

describe('createWikiPage Feature', () => {
  let client: any;
  const mockPut = jest.fn();
  const mockHandleRequestError = handleRequestError as jest.MockedFunction<
    typeof handleRequestError
  >;

  const defaultParams = {
    wikiId: 'test-wiki',
    content: 'Hello world',
    pagePath: '/',
  };

  beforeEach(() => {
    // Reset mocks for each test
    mockPut.mockReset();
    mockHandleRequestError.mockReset();

    client = {
      put: mockPut,
      defaults: {
        organizationId: 'defaultOrg',
        projectId: 'defaultProject',
      },
    };
  });

  it('should call client.put with correct URL and data for default org and project', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    await createWikiPage(defaultParams, client as any);

    expect(mockPut).toHaveBeenCalledTimes(1);
    expect(mockPut).toHaveBeenCalledWith(
      'defaultOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
      { content: 'Hello world' },
    );
  });

  it('should call client.put with correct URL when projectId is explicitly provided', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    const paramsWithProject = {
      ...defaultParams,
      projectId: 'customProject',
    };
    await createWikiPage(paramsWithProject, client as any);

    expect(mockPut).toHaveBeenCalledWith(
      'defaultOrg/customProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
      { content: 'Hello world' },
    );
  });

  it('should call client.put with correct URL when organizationId is explicitly provided', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    const paramsWithOrg = {
      ...defaultParams,
      organizationId: 'customOrg',
    };
    await createWikiPage(paramsWithOrg, client as any);

    expect(mockPut).toHaveBeenCalledWith(
      'customOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
      { content: 'Hello world' },
    );
  });

  it('should call client.put with correct URL when projectId is null (project-level wiki)', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    const paramsWithNullProject = {
      ...defaultParams,
      projectId: null, // Explicitly null for project-level resources that don't need a project
    };

    // Client default for projectId should also be null or undefined in this scenario
    const clientWithoutProject = {
      put: mockPut,
      defaults: {
        organizationId: 'defaultOrg',
        projectId: undefined,
      },
    };

    await createWikiPage(paramsWithNullProject, clientWithoutProject as any);

    expect(mockPut).toHaveBeenCalledWith(
      'defaultOrg/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
      { content: 'Hello world' },
    );
  });

  it('should correctly encode pagePath in the URL', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    const paramsWithPath = {
      ...defaultParams,
      pagePath: '/My Test Page/Sub Page',
    };
    await createWikiPage(paramsWithPath, client as any);

    expect(mockPut).toHaveBeenCalledWith(
      'defaultOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2FMy%20Test%20Page%2FSub%20Page&api-version=7.1-preview.1',
      { content: 'Hello world' },
    );
  });

  it('should use default pagePath "/" if pagePath is null', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    const paramsWithPath = {
      ...defaultParams,
      pagePath: null, // Explicitly null
    };
    await createWikiPage(paramsWithPath, client as any);

    expect(mockPut).toHaveBeenCalledWith(
      'defaultOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
      { content: 'Hello world' },
    );
  });

  it('should include comment in request body when provided', async () => {
    mockPut.mockResolvedValue({ data: { some: 'response' } });
    const paramsWithComment = {
      ...defaultParams,
      comment: 'Initial page creation',
    };
    await createWikiPage(paramsWithComment, client as any);

    expect(mockPut).toHaveBeenCalledWith(
      'defaultOrg/defaultProject/_apis/wiki/wikis/test-wiki/pages?path=%2F&api-version=7.1-preview.1',
      { content: 'Hello world', comment: 'Initial page creation' },
    );
  });

  it('should return the data from the response on success', async () => {
    const expectedResponse = { id: '123', path: '/', content: 'Hello world' };
    mockPut.mockResolvedValue({ data: expectedResponse });
    const result = await createWikiPage(defaultParams, client as any);

    expect(result).toEqual(expectedResponse);
  });

  // Skip this test for now as it requires complex mocking of environment variables
  it.skip('should throw if organizationId is not provided and not set in defaults', async () => {
    const clientWithoutOrg = {
      put: mockPut,
      defaults: {
        projectId: 'defaultProject',
        organizationId: undefined,
      },
    };

    const paramsNoOrg = {
      ...defaultParams,
      organizationId: null, // Explicitly null and no default
    };

    // This test is skipped because it requires complex mocking of environment variables
    // which is difficult to do in the current test setup
    await expect(
      createWikiPage(paramsNoOrg, clientWithoutOrg as any),
    ).rejects.toThrow(
      'Organization ID is not defined. Please provide it or set a default.',
    );
    expect(mockPut).not.toHaveBeenCalled();
  });

  it('should call handleRequestError if client.put throws an error', async () => {
    const error = new Error('API Error');
    mockPut.mockRejectedValue(error);
    mockHandleRequestError.mockImplementation(() => {
      throw new Error('Handled Error');
    });

    await expect(createWikiPage(defaultParams, client as any)).rejects.toThrow(
      'Handled Error',
    );
    expect(mockHandleRequestError).toHaveBeenCalledTimes(1);
    expect(mockHandleRequestError).toHaveBeenCalledWith(
      error,
      'Failed to create or update wiki page',
    );
  });
});

```

--------------------------------------------------------------------------------
/src/features/projects/get-project-details/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { getProjectDetails } from './feature';
import {
  getTestConnection,
  shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';

describe('getProjectDetails integration', () => {
  let connection: WebApi | null = null;
  let projectName: string;

  beforeAll(async () => {
    // Get a real connection using environment variables
    connection = await getTestConnection();
    projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
  });

  test('should retrieve basic project details from Azure DevOps', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Act - make an actual API call to Azure DevOps
    const result = await getProjectDetails(connection, {
      projectId: projectName,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.name).toBe(projectName);
    expect(result.id).toBeDefined();
    expect(result.url).toBeDefined();
    expect(result.state).toBeDefined();

    // Verify basic project structure
    expect(result.visibility).toBeDefined();
    expect(result.lastUpdateTime).toBeDefined();
    expect(result.capabilities).toBeDefined();
  });

  test('should retrieve project details with teams from Azure DevOps', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Act - make an actual API call to Azure DevOps
    const result = await getProjectDetails(connection, {
      projectId: projectName,
      includeTeams: true,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.teams).toBeDefined();
    expect(Array.isArray(result.teams)).toBe(true);

    // There should be at least one team (the default team)
    if (result.teams && result.teams.length > 0) {
      const team = result.teams[0];
      expect(team.id).toBeDefined();
      expect(team.name).toBeDefined();
      expect(team.url).toBeDefined();
    }
  });

  test('should retrieve project details with process information from Azure DevOps', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Act - make an actual API call to Azure DevOps
    const result = await getProjectDetails(connection, {
      projectId: projectName,
      includeProcess: true,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.process).toBeDefined();
    expect(result.process?.name).toBeDefined();
  });

  test('should retrieve project details with work item types from Azure DevOps', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Act - make an actual API call to Azure DevOps
    const result = await getProjectDetails(connection, {
      projectId: projectName,
      includeProcess: true,
      includeWorkItemTypes: true,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.process).toBeDefined();
    expect(result.process?.workItemTypes).toBeDefined();
    expect(Array.isArray(result.process?.workItemTypes)).toBe(true);

    // There should be at least one work item type
    if (
      result.process?.workItemTypes &&
      result.process.workItemTypes.length > 0
    ) {
      const workItemType = result.process.workItemTypes[0];
      expect(workItemType.name).toBeDefined();
      expect(workItemType.description).toBeDefined();
      expect(workItemType.states).toBeDefined();
    }
  });

  test('should retrieve project details with fields from Azure DevOps', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Act - make an actual API call to Azure DevOps
    const result = await getProjectDetails(connection, {
      projectId: projectName,
      includeProcess: true,
      includeWorkItemTypes: true,
      includeFields: true,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.process).toBeDefined();
    expect(result.process?.workItemTypes).toBeDefined();

    // There should be at least one work item type with fields
    if (
      result.process?.workItemTypes &&
      result.process.workItemTypes.length > 0
    ) {
      const workItemType = result.process.workItemTypes[0];
      expect(workItemType.fields).toBeDefined();
      expect(Array.isArray(workItemType.fields)).toBe(true);

      // There should be at least one field (like Title)
      if (workItemType.fields && workItemType.fields.length > 0) {
        const field = workItemType.fields[0];
        expect(field.name).toBeDefined();
        expect(field.referenceName).toBeDefined();
      }
    }
  });

  test('should throw error when project is not found', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Use a non-existent project name
    const nonExistentProjectName = 'non-existent-project-' + Date.now();

    // Act & Assert - should throw an error for non-existent project
    await expect(
      getProjectDetails(connection, {
        projectId: nonExistentProjectName,
      }),
    ).rejects.toThrow(/not found|Failed to get project/);
  });
});

```

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

```typescript
// Re-export schemas and types
export * from './schemas';
export * from './types';

// Re-export features
export * from './get-repository';
export * from './get-repository-details';
export * from './list-repositories';
export * from './get-file-content';
export * from './get-all-repositories-tree';
export * from './get-repository-tree';
export * from './create-branch';
export * from './create-commit';
export * from './list-commits';

// Export tool definitions
export * from './tool-definitions';

// New exports for request handling
import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { WebApi } from 'azure-devops-node-api';
import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';
import {
  RequestIdentifier,
  RequestHandler,
} from '../../shared/types/request-handler';
import { defaultProject, defaultOrg } from '../../utils/environment';
import {
  GetRepositorySchema,
  GetRepositoryDetailsSchema,
  ListRepositoriesSchema,
  GetFileContentSchema,
  GetAllRepositoriesTreeSchema,
  GetRepositoryTreeSchema,
  CreateBranchSchema,
  CreateCommitSchema,
  ListCommitsSchema,
  getRepository,
  getRepositoryDetails,
  listRepositories,
  getFileContent,
  getAllRepositoriesTree,
  getRepositoryTree,
  createBranch,
  createCommit,
  listCommits,
  formatRepositoryTree,
} from './';

/**
 * Checks if the request is for the repositories feature
 */
export const isRepositoriesRequest: RequestIdentifier = (
  request: CallToolRequest,
): boolean => {
  const toolName = request.params.name;
  return [
    'get_repository',
    'get_repository_details',
    'list_repositories',
    'get_file_content',
    'get_all_repositories_tree',
    'get_repository_tree',
    'create_branch',
    'create_commit',
    'list_commits',
  ].includes(toolName);
};

/**
 * Handles repositories feature requests
 */
export const handleRepositoriesRequest: RequestHandler = async (
  connection: WebApi,
  request: CallToolRequest,
): Promise<{ content: Array<{ type: string; text: string }> }> => {
  switch (request.params.name) {
    case 'get_repository': {
      const args = GetRepositorySchema.parse(request.params.arguments);
      const result = await getRepository(
        connection,
        args.projectId ?? defaultProject,
        args.repositoryId,
      );
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'get_repository_details': {
      const args = GetRepositoryDetailsSchema.parse(request.params.arguments);
      const result = await getRepositoryDetails(connection, {
        projectId: args.projectId ?? defaultProject,
        repositoryId: args.repositoryId,
        includeStatistics: args.includeStatistics,
        includeRefs: args.includeRefs,
        refFilter: args.refFilter,
        branchName: args.branchName,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'list_repositories': {
      const args = ListRepositoriesSchema.parse(request.params.arguments);
      const result = await listRepositories(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'get_file_content': {
      const args = GetFileContentSchema.parse(request.params.arguments);

      // Map the string version type to the GitVersionType enum
      let versionTypeEnum: GitVersionType | undefined;
      if (args.versionType && args.version) {
        if (args.versionType === 'branch') {
          versionTypeEnum = GitVersionType.Branch;
        } else if (args.versionType === 'commit') {
          versionTypeEnum = GitVersionType.Commit;
        } else if (args.versionType === 'tag') {
          versionTypeEnum = GitVersionType.Tag;
        }
      }

      const result = await getFileContent(
        connection,
        args.projectId ?? defaultProject,
        args.repositoryId,
        args.path,
        versionTypeEnum !== undefined && args.version
          ? { versionType: versionTypeEnum, version: args.version }
          : undefined,
      );
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'get_all_repositories_tree': {
      const args = GetAllRepositoriesTreeSchema.parse(request.params.arguments);
      const result = await getAllRepositoriesTree(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
        organizationId: args.organizationId ?? defaultOrg,
      });

      // Format the output as plain text tree representation
      let formattedOutput = '';
      for (const repo of result.repositories) {
        formattedOutput += formatRepositoryTree(
          repo.name,
          repo.tree,
          repo.stats,
          repo.error,
        );
        formattedOutput += '\n'; // Add blank line between repositories
      }

      return {
        content: [{ type: 'text', text: formattedOutput }],
      };
    }
    case 'get_repository_tree': {
      const args = GetRepositoryTreeSchema.parse(request.params.arguments);
      const result = await getRepositoryTree(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(result, null, 2),
          },
        ],
      };
    }
    case 'create_branch': {
      const args = CreateBranchSchema.parse(request.params.arguments);
      await createBranch(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [{ type: 'text', text: 'Branch created successfully' }],
      };
    }
    case 'create_commit': {
      const args = CreateCommitSchema.parse(request.params.arguments);
      await createCommit(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [{ type: 'text', text: 'Commit created successfully' }],
      };
    }
    case 'list_commits': {
      const args = ListCommitsSchema.parse(request.params.arguments);
      const result = await listCommits(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    default:
      throw new Error(`Unknown repositories tool: ${request.params.name}`);
  }
};

```

--------------------------------------------------------------------------------
/project-management/planning/the-dream-team.md:
--------------------------------------------------------------------------------

```markdown
Below is the **Dream Team Documentation** for building the Azure DevOps MCP server. This document outlines the ideal roles and skill sets required to ensure the project's success, from development to deployment. Each role is carefully selected to address the technical, security, and operational challenges of building a robust, AI-integrated server.

---

## Dream Team Documentation: Building the Azure DevOps MCP Server

### Overview

The Azure DevOps MCP server is a complex tool that requires a multidisciplinary team with expertise in software development, Azure DevOps, security, testing, documentation, project management, and AI integration. The following roles are essential to ensure the server is built efficiently, securely, and in alignment with the Model Context Protocol (MCP) standards.

### Key Roles and Responsibilities

#### 1. **Full-Stack Developer (Typescript/Node.js)**

- **Responsibilities**:
  - Implement the server's core functionality using Typescript and Node.js.
  - Develop and maintain MCP tools (e.g., `list_projects`, `create_work_item`).
  - Write tests as part of the implementation process (TDD).
  - Integrate with the MCP Typescript SDK and Azure DevOps APIs.
  - Write clean, modular, and efficient code following best practices.
  - Ensure code quality through comprehensive unit and integration tests.
  - Build automated testing pipelines for continuous integration.
  - Perform integration testing across components.
- **Required Skills**:
  - Proficiency in Typescript and Node.js.
  - Strong testing skills and experience with test frameworks (e.g., Jest).
  - Experience writing testable code and following TDD practices.
  - Experience with REST APIs and asynchronous programming.
  - Familiarity with Git and version control systems.
  - Understanding of modular software design.
  - Experience with API testing and mocking tools.

#### 2. **Azure DevOps API Expert**

- **Responsibilities**:
  - Guide the team on effectively using Azure DevOps REST APIs (e.g., Git, Work Item Tracking, Build).
  - Ensure the server leverages Azure DevOps features optimally (e.g., repository operations, pipelines).
  - Assist in mapping MCP tools to the correct API endpoints.
  - Troubleshoot API-related issues and optimize API usage.
  - Help develop tests for Azure DevOps API integrations.
- **Required Skills**:
  - Deep understanding of Azure DevOps services and their REST APIs.
  - Experience with Azure DevOps workflows (e.g., repositories, work items, pipelines).
  - Knowledge of Azure DevOps authentication mechanisms (PAT, AAD).
  - Ability to interpret API documentation and handle rate limits.
  - Experience testing API integrations.

#### 3. **Security Specialist**

- **Responsibilities**:
  - Design and implement secure authentication methods (PAT and AAD).
  - Ensure credentials are stored and managed securely (e.g., environment variables).
  - Scope permissions to the minimum required for each tool.
  - Implement error handling and logging without exposing sensitive data.
  - Conduct security reviews and recommend improvements.
  - Develop security tests and validation procedures.
- **Required Skills**:
  - Expertise in API security, authentication, and authorization.
  - Familiarity with Azure Active Directory and PAT management.
  - Knowledge of secure coding practices and vulnerability prevention.
  - Experience with logging, auditing, and compliance.
  - Experience with security testing tools and methodologies.

#### 4. **Technical Writer**

- **Responsibilities**:
  - Create comprehensive documentation, including setup guides, tool descriptions, and usage examples.
  - Write clear API references and troubleshooting tips.
  - Ensure documentation is accessible to both technical and non-technical users.
  - Maintain up-to-date documentation as the server evolves.
- **Required Skills**:
  - Strong technical writing and communication skills.
  - Ability to explain complex concepts simply.
  - Experience documenting APIs and developer tools.
  - Familiarity with Markdown and documentation platforms (e.g., GitHub README).

#### 5. **Project Manager**

- **Responsibilities**:
  - Coordinate the team's efforts and manage the project timeline.
  - Track progress using Azure Boards or similar tools.
  - Facilitate communication and resolve blockers.
  - Ensure the project stays on scope and meets deadlines.
  - Manage stakeholder expectations and provide status updates.
- **Required Skills**:
  - Experience in agile project management.
  - Proficiency with project tracking tools (e.g., Azure Boards, Jira).
  - Strong organizational and leadership skills.
  - Ability to manage remote or distributed teams.

#### 6. **AI Integration Consultant**

- **Responsibilities**:
  - Advise on how the server can best integrate with AI models (e.g., Claude Desktop).
  - Ensure tools are designed to support AI-driven workflows (e.g., user story to pull request).
  - Provide insights into MCP's AI integration capabilities.
  - Assist in testing AI interactions with the server.
- **Required Skills**:
  - Experience with AI model integration and workflows.
  - Understanding of the Model Context Protocol (MCP).
  - Familiarity with AI tools like Claude Desktop.
  - Ability to bridge AI and software development domains.

---

### Team Structure and Collaboration

- **Core Team**: Full-Stack Developer, Azure DevOps API Expert, Security Specialist.
- **Support Roles**: Technical Writer, Project Manager, AI Integration Consultant.
- **Collaboration**: Use Agile methodologies with bi-weekly sprints, daily stand-ups, and regular retrospectives to iterate efficiently.
- **Communication Tools**: Slack or Microsoft Teams for real-time communication, Azure Boards for task tracking, and GitHub/Azure DevOps for version control and code reviews.

---

### Why This Team?

Each role addresses a critical aspect of the project:

- The **Full-Stack Developer** builds the server using modern technologies like Typescript and Node.js, integrating testing throughout the development process.
- The **Azure DevOps API Expert** ensures seamless integration with Azure DevOps services.
- The **Security Specialist** safeguards the server against vulnerabilities.
- The **Technical Writer** makes the server user-friendly with clear documentation.
- The **Project Manager** keeps the team aligned and on schedule.
- The **AI Integration Consultant** ensures the server meets AI-driven workflow requirements.

This dream team combines technical expertise, security, integrated quality assurance, and project management to deliver a high-quality, secure, and user-friendly Azure DevOps MCP server. Testing is built into our development process, not treated as a separate concern.

```

--------------------------------------------------------------------------------
/src/features/repositories/get-repository-details/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { getRepositoryDetails } from './feature';
import {
  getTestConnection,
  shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';

describe('getRepositoryDetails integration', () => {
  let connection: WebApi | null = null;
  let projectName: string;

  beforeAll(async () => {
    // Get a real connection using environment variables
    connection = await getTestConnection();
    projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
  });

  test('should retrieve repository details from Azure DevOps', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // First, get a list of repos to find one to test with
    const gitApi = await connection.getGitApi();
    const repos = await gitApi.getRepositories(projectName);

    // Skip if no repos are available
    if (!repos || repos.length === 0) {
      console.log('Skipping test: No repositories available in the project');
      return;
    }

    // Use the first repo as a test subject
    const testRepo = repos[0];

    // Act - make an actual API call to Azure DevOps
    const result = await getRepositoryDetails(connection, {
      projectId: projectName,
      repositoryId: testRepo.name || testRepo.id || '',
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.repository).toBeDefined();
    expect(result.repository.id).toBe(testRepo.id);
    expect(result.repository.name).toBe(testRepo.name);
    expect(result.repository.project).toBeDefined();
    if (result.repository.project) {
      expect(result.repository.project.name).toBe(projectName);
    }
  });

  test('should retrieve repository details with statistics', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // First, get a list of repos to find one to test with
    const gitApi = await connection.getGitApi();
    const repos = await gitApi.getRepositories(projectName);

    // Skip if no repos are available
    if (!repos || repos.length === 0) {
      console.log('Skipping test: No repositories available in the project');
      return;
    }

    // Use the first repo as a test subject
    const testRepo = repos[0];

    // Act - make an actual API call to Azure DevOps
    const result = await getRepositoryDetails(connection, {
      projectId: projectName,
      repositoryId: testRepo.name || testRepo.id || '',
      includeStatistics: true,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.repository).toBeDefined();
    expect(result.repository.id).toBe(testRepo.id);
    expect(result.statistics).toBeDefined();
    expect(Array.isArray(result.statistics?.branches)).toBe(true);
  });

  test('should retrieve repository details with refs', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // First, get a list of repos to find one to test with
    const gitApi = await connection.getGitApi();
    const repos = await gitApi.getRepositories(projectName);

    // Skip if no repos are available
    if (!repos || repos.length === 0) {
      console.log('Skipping test: No repositories available in the project');
      return;
    }

    // Use the first repo as a test subject
    const testRepo = repos[0];

    // Act - make an actual API call to Azure DevOps
    const result = await getRepositoryDetails(connection, {
      projectId: projectName,
      repositoryId: testRepo.name || testRepo.id || '',
      includeRefs: true,
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.repository).toBeDefined();
    expect(result.repository.id).toBe(testRepo.id);
    expect(result.refs).toBeDefined();
    expect(result.refs?.value).toBeDefined();
    expect(Array.isArray(result.refs?.value)).toBe(true);
    expect(typeof result.refs?.count).toBe('number');
  });

  test('should retrieve repository details with refs filtered by heads/', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // First, get a list of repos to find one to test with
    const gitApi = await connection.getGitApi();
    const repos = await gitApi.getRepositories(projectName);

    // Skip if no repos are available
    if (!repos || repos.length === 0) {
      console.log('Skipping test: No repositories available in the project');
      return;
    }

    // Use the first repo as a test subject
    const testRepo = repos[0];

    // Act - make an actual API call to Azure DevOps
    const result = await getRepositoryDetails(connection, {
      projectId: projectName,
      repositoryId: testRepo.name || testRepo.id || '',
      includeRefs: true,
      refFilter: 'heads/',
    });

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.repository).toBeDefined();
    expect(result.refs).toBeDefined();
    expect(result.refs?.value).toBeDefined();

    // All refs should start with refs/heads/
    if (result.refs && result.refs.value.length > 0) {
      result.refs.value.forEach((ref) => {
        expect(ref.name).toMatch(/^refs\/heads\//);
      });
    }
  });

  test('should throw error when repository is not found', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // This connection must be available if we didn't skip
    if (!connection) {
      throw new Error(
        'Connection should be available when test is not skipped',
      );
    }

    // Use a non-existent repository name
    const nonExistentRepoName = 'non-existent-repo-' + Date.now();

    // Act & Assert - should throw an error for non-existent repo
    await expect(
      getRepositoryDetails(connection, {
        projectId: projectName,
        repositoryId: nonExistentRepoName,
      }),
    ).rejects.toThrow(/not found|Failed to get repository/);
  });
});

```

--------------------------------------------------------------------------------
/project-management/planning/azure-identity-authentication-design.md:
--------------------------------------------------------------------------------

```markdown
# Azure Identity Authentication for Azure DevOps MCP Server

This document outlines the implementation approach for adding Azure Identity authentication support to the Azure DevOps MCP Server.

## Overview

The Azure DevOps MCP Server currently supports Personal Access Token (PAT) authentication. This enhancement will add support for Azure Identity authentication methods, specifically DefaultAzureCredential and AzureCliCredential, to provide more flexible authentication options for different environments.

## Azure Identity SDK

The `@azure/identity` package provides various credential types for authenticating with Azure services. For our implementation, we will focus on the following credential types:

### DefaultAzureCredential

`DefaultAzureCredential` provides a simplified authentication experience by trying multiple credential types in sequence:

1. Environment variables (EnvironmentCredential)
2. Managed Identity (ManagedIdentityCredential)
3. Azure CLI (AzureCliCredential)
4. Visual Studio Code (VisualStudioCodeCredential)
5. Azure PowerShell (AzurePowerShellCredential)
6. Interactive Browser (InteractiveBrowserCredential) - optional, disabled by default

This makes it ideal for applications that need to work in different environments (local development, Azure-hosted) without code changes.

### AzureCliCredential

`AzureCliCredential` authenticates using the Azure CLI's logged-in account. It requires the Azure CLI to be installed and the user to be logged in (`az login`). This is particularly useful for local development scenarios where developers are already using the Azure CLI.

## Implementation Approach

### 1. Authentication Abstraction Layer

Create an abstraction layer for authentication that supports both PAT and Azure Identity methods:

```typescript
// src/api/auth.ts
export interface AuthProvider {
  getConnection(): Promise<WebApi>;
  isAuthenticated(): Promise<boolean>;
}

export class PatAuthProvider implements AuthProvider {
  // Existing PAT authentication implementation
}

export class AzureIdentityAuthProvider implements AuthProvider {
  // New Azure Identity authentication implementation
}
```

### 2. Authentication Factory

Implement a factory pattern to create the appropriate authentication provider based on configuration:

```typescript
// src/api/auth.ts
export enum AuthMethod {
  PAT = 'pat',
  AZURE_IDENTITY = 'azure-identity',
}

export function createAuthProvider(config: AzureDevOpsConfig): AuthProvider {
  switch (config.authMethod) {
    case AuthMethod.AZURE_IDENTITY:
      return new AzureIdentityAuthProvider(config);
    case AuthMethod.PAT:
    default:
      return new PatAuthProvider(config);
  }
}
```

### 3. Azure Identity Authentication Provider

Implement the Azure Identity authentication provider:

```typescript
// src/api/auth.ts
export class AzureIdentityAuthProvider implements AuthProvider {
  private config: AzureDevOpsConfig;
  private connectionPromise: Promise<WebApi> | null = null;

  constructor(config: AzureDevOpsConfig) {
    this.config = config;
  }

  async getConnection(): Promise<WebApi> {
    if (!this.connectionPromise) {
      this.connectionPromise = this.createConnection();
    }
    return this.connectionPromise;
  }

  private async createConnection(): Promise<WebApi> {
    try {
      // Azure DevOps resource ID for token scope
      const azureDevOpsResourceId = '499b84ac-1321-427f-aa17-267ca6975798';

      // Create credential based on configuration
      const credential = this.createCredential();

      // Get token for Azure DevOps
      const token = await credential.getToken(
        `${azureDevOpsResourceId}/.default`,
      );

      if (!token) {
        throw new AzureDevOpsAuthenticationError(
          'Failed to acquire token from Azure Identity',
        );
      }

      // Create auth handler with token
      const authHandler = new BearerCredentialHandler(token.token);

      // Create WebApi client
      const connection = new WebApi(this.config.organizationUrl, authHandler);

      // Test the connection
      await connection.getLocationsApi();

      return connection;
    } catch (error) {
      throw new AzureDevOpsAuthenticationError(
        `Failed to authenticate with Azure Identity: ${error instanceof Error ? error.message : String(error)}`,
      );
    }
  }

  private createCredential(): TokenCredential {
    if (this.config.azureIdentityOptions?.useAzureCliCredential) {
      return new AzureCliCredential();
    }

    // Default to DefaultAzureCredential
    return new DefaultAzureCredential();
  }

  async isAuthenticated(): Promise<boolean> {
    try {
      await this.getConnection();
      return true;
    } catch {
      return false;
    }
  }
}
```

### 4. Configuration Updates

Update the configuration interface to support specifying the authentication method:

```typescript
// src/types/config.ts
export interface AzureDevOpsConfig {
  // Existing properties
  organizationUrl: string;
  personalAccessToken?: string;
  defaultProject?: string;
  apiVersion?: string;

  // New properties
  authMethod?: AuthMethod;
  azureIdentityOptions?: {
    useAzureCliCredential?: boolean;
    // Other Azure Identity options as needed
  };
}
```

### 5. Environment Variable Updates

Update the environment variable handling in `index.ts`:

```typescript
// src/index.ts
const config: AzureDevOpsConfig = {
  organizationUrl: process.env.AZURE_DEVOPS_ORG_URL || '',
  personalAccessToken: process.env.AZURE_DEVOPS_PAT,
  defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT,
  apiVersion: process.env.AZURE_DEVOPS_API_VERSION,
  authMethod:
    (process.env.AZURE_DEVOPS_AUTH_METHOD as AuthMethod) || AuthMethod.PAT,
  azureIdentityOptions: {
    useAzureCliCredential:
      process.env.AZURE_DEVOPS_USE_CLI_CREDENTIAL === 'true',
  },
};
```

### 6. Client Updates

Update the `AzureDevOpsClient` class to use the authentication provider:

```typescript
// src/api/client.ts
export class AzureDevOpsClient {
  private authProvider: AuthProvider;

  constructor(config: AzureDevOpsConfig) {
    this.authProvider = createAuthProvider(config);
  }

  private async getClient(): Promise<WebApi> {
    return this.authProvider.getConnection();
  }

  // Rest of the class remains the same
}
```

## Error Handling

Implement proper error handling for Azure Identity authentication failures:

```typescript
// src/common/errors.ts
export class AzureIdentityAuthenticationError extends AzureDevOpsAuthenticationError {
  constructor(message: string) {
    super(`Azure Identity Authentication Error: ${message}`);
  }
}
```

## Configuration Examples

### PAT Authentication

```env
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-org
AZURE_DEVOPS_PAT=your-pat
AZURE_DEVOPS_AUTH_METHOD=pat
```

### DefaultAzureCredential Authentication

```env
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-org
AZURE_DEVOPS_AUTH_METHOD=azure-identity
# Optional environment variables for specific credential types
AZURE_TENANT_ID=your-tenant-id
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
```

### AzureCliCredential Authentication

```env
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-org
AZURE_DEVOPS_AUTH_METHOD=azure-cli
``` 
```

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

```typescript
import { isWorkItemsRequest, handleWorkItemsRequest } from './';
import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { WebApi } from 'azure-devops-node-api';
import * as workItemModule from './';

// Mock the imported modules
jest.mock('./get-work-item', () => ({
  getWorkItem: jest.fn(),
}));

jest.mock('./list-work-items', () => ({
  listWorkItems: jest.fn(),
}));

jest.mock('./create-work-item', () => ({
  createWorkItem: jest.fn(),
}));

jest.mock('./update-work-item', () => ({
  updateWorkItem: jest.fn(),
}));

jest.mock('./manage-work-item-link', () => ({
  manageWorkItemLink: jest.fn(),
}));

// Helper function to create a valid CallToolRequest object
const createCallToolRequest = (name: string, args: any): CallToolRequest => {
  return {
    method: 'tools/call',
    params: {
      name,
      arguments: args,
    },
  } as unknown as CallToolRequest;
};

describe('Work Items Request Handlers', () => {
  describe('isWorkItemsRequest', () => {
    it('should return true for work items requests', () => {
      const workItemsRequests = [
        'get_work_item',
        'list_work_items',
        'create_work_item',
        'update_work_item',
        'manage_work_item_link',
      ];

      workItemsRequests.forEach((name) => {
        const request = createCallToolRequest(name, {});

        expect(isWorkItemsRequest(request)).toBe(true);
      });
    });

    it('should return false for non-work items requests', () => {
      const request = createCallToolRequest('get_project', {});

      expect(isWorkItemsRequest(request)).toBe(false);
    });
  });

  describe('handleWorkItemsRequest', () => {
    let mockConnection: WebApi;

    beforeEach(() => {
      mockConnection = {} as WebApi;

      // Setup mock for schema validation - with correct return types
      jest
        .spyOn(workItemModule.GetWorkItemSchema, 'parse')
        .mockImplementation(() => {
          return { workItemId: 123, expand: undefined };
        });

      jest
        .spyOn(workItemModule.ListWorkItemsSchema, 'parse')
        .mockImplementation(() => {
          return { projectId: 'myProject' };
        });

      jest
        .spyOn(workItemModule.CreateWorkItemSchema, 'parse')
        .mockImplementation(() => {
          return {
            projectId: 'myProject',
            workItemType: 'Task',
            title: 'New Task',
          };
        });

      jest
        .spyOn(workItemModule.UpdateWorkItemSchema, 'parse')
        .mockImplementation(() => {
          return {
            workItemId: 123,
            title: 'Updated Title',
          };
        });

      jest
        .spyOn(workItemModule.ManageWorkItemLinkSchema, 'parse')
        .mockImplementation(() => {
          return {
            sourceWorkItemId: 123,
            targetWorkItemId: 456,
            operation: 'add' as 'add' | 'remove' | 'update',
            relationType: 'System.LinkTypes.Hierarchy-Forward',
          };
        });

      // Setup mocks for feature functions
      jest.spyOn(workItemModule, 'getWorkItem').mockResolvedValue({ id: 123 });
      jest
        .spyOn(workItemModule, 'listWorkItems')
        .mockResolvedValue([{ id: 123 }, { id: 456 }]);
      jest
        .spyOn(workItemModule, 'createWorkItem')
        .mockResolvedValue({ id: 789 });
      jest
        .spyOn(workItemModule, 'updateWorkItem')
        .mockResolvedValue({ id: 123 });
      jest
        .spyOn(workItemModule, 'manageWorkItemLink')
        .mockResolvedValue({ id: 123 });
    });

    afterEach(() => {
      jest.resetAllMocks();
    });

    it('should handle get_work_item requests', async () => {
      const request = createCallToolRequest('get_work_item', {
        workItemId: 123,
      });

      const result = await handleWorkItemsRequest(mockConnection, request);

      expect(workItemModule.GetWorkItemSchema.parse).toHaveBeenCalledWith({
        workItemId: 123,
      });
      expect(workItemModule.getWorkItem).toHaveBeenCalledWith(
        mockConnection,
        123,
        undefined,
      );
      expect(result).toEqual({
        content: [{ type: 'text', text: JSON.stringify({ id: 123 }, null, 2) }],
      });
    });

    it('should handle list_work_items requests', async () => {
      const request = createCallToolRequest('list_work_items', {
        projectId: 'myProject',
      });

      const result = await handleWorkItemsRequest(mockConnection, request);

      expect(workItemModule.ListWorkItemsSchema.parse).toHaveBeenCalledWith({
        projectId: 'myProject',
      });
      expect(workItemModule.listWorkItems).toHaveBeenCalled();
      expect(result).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify([{ id: 123 }, { id: 456 }], null, 2),
          },
        ],
      });
    });

    it('should handle create_work_item requests', async () => {
      const request = createCallToolRequest('create_work_item', {
        projectId: 'myProject',
        workItemType: 'Task',
        title: 'New Task',
      });

      const result = await handleWorkItemsRequest(mockConnection, request);

      expect(workItemModule.CreateWorkItemSchema.parse).toHaveBeenCalledWith({
        projectId: 'myProject',
        workItemType: 'Task',
        title: 'New Task',
      });
      expect(workItemModule.createWorkItem).toHaveBeenCalled();
      expect(result).toEqual({
        content: [{ type: 'text', text: JSON.stringify({ id: 789 }, null, 2) }],
      });
    });

    it('should handle update_work_item requests', async () => {
      const request = createCallToolRequest('update_work_item', {
        workItemId: 123,
        title: 'Updated Title',
      });

      const result = await handleWorkItemsRequest(mockConnection, request);

      expect(workItemModule.UpdateWorkItemSchema.parse).toHaveBeenCalledWith({
        workItemId: 123,
        title: 'Updated Title',
      });
      expect(workItemModule.updateWorkItem).toHaveBeenCalled();
      expect(result).toEqual({
        content: [{ type: 'text', text: JSON.stringify({ id: 123 }, null, 2) }],
      });
    });

    it('should handle manage_work_item_link requests', async () => {
      const request = createCallToolRequest('manage_work_item_link', {
        sourceWorkItemId: 123,
        targetWorkItemId: 456,
        operation: 'add',
        relationType: 'System.LinkTypes.Hierarchy-Forward',
      });

      const result = await handleWorkItemsRequest(mockConnection, request);

      expect(
        workItemModule.ManageWorkItemLinkSchema.parse,
      ).toHaveBeenCalledWith({
        sourceWorkItemId: 123,
        targetWorkItemId: 456,
        operation: 'add',
        relationType: 'System.LinkTypes.Hierarchy-Forward',
      });
      expect(workItemModule.manageWorkItemLink).toHaveBeenCalled();
      expect(result).toEqual({
        content: [{ type: 'text', text: JSON.stringify({ id: 123 }, null, 2) }],
      });
    });

    it('should throw an error for unknown work items tools', async () => {
      const request = createCallToolRequest('unknown_tool', {});

      await expect(
        handleWorkItemsRequest(mockConnection, request),
      ).rejects.toThrow('Unknown work items tool: unknown_tool');
    });
  });
});

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { getWorkItem } from './feature';
import {
  getTestConnection,
  shouldSkipIntegrationTest,
} from '../__test__/test-helpers';

import { AzureDevOpsResourceNotFoundError } from '../../../shared/errors';
import { createWorkItem } from '../create-work-item/feature';
import { manageWorkItemLink } from '../manage-work-item-link/feature';
import { CreateWorkItemOptions } from '../types';

describe('getWorkItem integration', () => {
  let connection: WebApi | null = null;
  let testWorkItemId: number | null = null;
  let linkedWorkItemId: number | null = null;
  let projectName: string;

  beforeAll(async () => {
    // Get a real connection using environment variables
    connection = await getTestConnection();
    projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';

    // Skip setup if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      return;
    }

    try {
      // Create a test work item
      const uniqueTitle = `Test Work Item ${new Date().toISOString()}`;
      const options: CreateWorkItemOptions = {
        title: uniqueTitle,
        description: 'Test work item for get-work-item integration tests',
      };

      const testWorkItem = await createWorkItem(
        connection,
        projectName,
        'Task',
        options,
      );

      // Create another work item to link to the first one
      const linkedItemOptions: CreateWorkItemOptions = {
        title: `Linked Work Item ${new Date().toISOString()}`,
        description: 'Linked work item for get-work-item integration tests',
      };

      const linkedWorkItem = await createWorkItem(
        connection,
        projectName,
        'Task',
        linkedItemOptions,
      );

      if (testWorkItem?.id && linkedWorkItem?.id) {
        testWorkItemId = testWorkItem.id;
        linkedWorkItemId = linkedWorkItem.id;

        // Create a link between the two work items
        await manageWorkItemLink(connection, projectName, {
          sourceWorkItemId: testWorkItemId,
          targetWorkItemId: linkedWorkItemId,
          operation: 'add',
          relationType: 'System.LinkTypes.Related',
          comment: 'Link created for get-work-item integration tests',
        });
      }
    } catch (error) {
      console.error('Failed to create test work items:', error);
    }
  });

  test('should retrieve a real work item from Azure DevOps with default expand=all', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest() || !connection || !testWorkItemId) {
      return;
    }

    // Act - get work item by ID
    const result = await getWorkItem(connection, testWorkItemId);

    // Assert
    expect(result).toBeDefined();
    expect(result.id).toBe(testWorkItemId);

    // Verify expanded fields and data are present
    expect(result.fields).toBeDefined();
    expect(result._links).toBeDefined();

    // With expand=all and a linked item, relations should be defined
    expect(result.relations).toBeDefined();

    if (result.fields) {
      // Verify common fields that should be present with expand=all
      expect(result.fields['System.Title']).toBeDefined();
      expect(result.fields['System.State']).toBeDefined();
      expect(result.fields['System.CreatedDate']).toBeDefined();
      expect(result.fields['System.ChangedDate']).toBeDefined();
    }
  });

  test('should retrieve work item with expanded relations', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest() || !connection || !testWorkItemId) {
      return;
    }

    // Act - get work item with relations expansion
    const result = await getWorkItem(connection, testWorkItemId, 'relations');

    // Assert
    expect(result).toBeDefined();
    expect(result.id).toBe(testWorkItemId);

    // When using expand=relations on a work item with links, relations should be defined
    expect(result.relations).toBeDefined();

    // Verify we can access the related work item
    if (result.relations && result.relations.length > 0) {
      const relation = result.relations[0];
      expect(relation.rel).toBe('System.LinkTypes.Related');
      expect(relation.url).toContain(linkedWorkItemId?.toString());
    }

    // Verify fields exist
    expect(result.fields).toBeDefined();
    if (result.fields) {
      expect(result.fields['System.Title']).toBeDefined();
    }
  });

  test('should retrieve work item with minimal fields when using expand=none', async () => {
    if (shouldSkipIntegrationTest() || !connection || !testWorkItemId) {
      return;
    }

    // Act - get work item with no expansion
    const result = await getWorkItem(connection, testWorkItemId, 'none');

    // Assert
    expect(result).toBeDefined();
    expect(result.id).toBe(testWorkItemId);
    expect(result.fields).toBeDefined();

    // With expand=none, we should still get _links but no relations
    // The Azure DevOps API still returns _links even with expand=none
    expect(result.relations).toBeUndefined();
  });

  test('should throw AzureDevOpsResourceNotFoundError for non-existent work item', async () => {
    if (shouldSkipIntegrationTest() || !connection) {
      return;
    }

    // Use a very large ID that's unlikely to exist
    const nonExistentId = 999999999;

    // Assert that it throws the correct error
    await expect(getWorkItem(connection, nonExistentId)).rejects.toThrow(
      AzureDevOpsResourceNotFoundError,
    );
  });

  test('should include all possible fields with null values for empty fields', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest() || !connection || !testWorkItemId) {
      return;
    }

    // Act - get work item by ID
    const result = await getWorkItem(connection, testWorkItemId);

    // Assert
    expect(result).toBeDefined();
    expect(result.fields).toBeDefined();

    if (result.fields) {
      // Get a direct connection to WorkItemTrackingApi to fetch field info for comparison
      const witApi = await connection.getWorkItemTrackingApi();
      const projectName = result.fields['System.TeamProject'];
      const workItemType = result.fields['System.WorkItemType'];

      expect(projectName).toBeDefined();
      expect(workItemType).toBeDefined();

      if (projectName && workItemType) {
        // Get all possible field references for this work item type
        const allFields = await witApi.getWorkItemTypeFieldsWithReferences(
          projectName.toString(),
          workItemType.toString(),
        );

        // Check that all fields from the reference are present in the result
        // Some might be null, but they should exist in the fields object
        for (const field of allFields) {
          if (field.referenceName) {
            expect(Object.keys(result.fields)).toContain(field.referenceName);
          }
        }

        // There should be at least one field with a null value
        // (This is a probabilistic test but very likely to pass since work items
        // typically have many optional fields that aren't filled in)
        const hasNullField = Object.values(result.fields).some(
          (value) => value === null,
        );
        expect(hasNullField).toBe(true);
      }
    }
  });
});

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { isWikisRequest, handleWikisRequest } from './index';
import { getWikis, GetWikisSchema } from './get-wikis';
import { getWikiPage, GetWikiPageSchema } from './get-wiki-page';
import { createWiki, CreateWikiSchema, WikiType } from './create-wiki';
import { updateWikiPage, UpdateWikiPageSchema } from './update-wiki-page';

// Mock the imported modules
jest.mock('./get-wikis', () => ({
  getWikis: jest.fn(),
  GetWikisSchema: {
    parse: jest.fn(),
  },
}));

jest.mock('./get-wiki-page', () => ({
  getWikiPage: jest.fn(),
  GetWikiPageSchema: {
    parse: jest.fn(),
  },
}));

jest.mock('./create-wiki', () => ({
  createWiki: jest.fn(),
  CreateWikiSchema: {
    parse: jest.fn(),
  },
  WikiType: {
    ProjectWiki: 'projectWiki',
    CodeWiki: 'codeWiki',
  },
}));

jest.mock('./update-wiki-page', () => ({
  updateWikiPage: jest.fn(),
  UpdateWikiPageSchema: {
    parse: jest.fn(),
  },
}));

describe('Wikis Request Handlers', () => {
  const mockConnection = {} as WebApi;

  describe('isWikisRequest', () => {
    it('should return true for wikis requests', () => {
      const validTools = [
        'get_wikis',
        'get_wiki_page',
        'create_wiki',
        'update_wiki_page',
      ];
      validTools.forEach((tool) => {
        const request = {
          params: { name: tool, arguments: {} },
          method: 'tools/call',
        } as CallToolRequest;
        expect(isWikisRequest(request)).toBe(true);
      });
    });

    it('should return false for non-wikis requests', () => {
      const request = {
        params: { name: 'list_projects', arguments: {} },
        method: 'tools/call',
      } as CallToolRequest;
      expect(isWikisRequest(request)).toBe(false);
    });
  });

  describe('handleWikisRequest', () => {
    it('should handle get_wikis request', async () => {
      const mockWikis = [
        { id: 'wiki1', name: 'Wiki 1' },
        { id: 'wiki2', name: 'Wiki 2' },
      ];
      (getWikis as jest.Mock).mockResolvedValue(mockWikis);

      const request = {
        params: {
          name: 'get_wikis',
          arguments: {
            projectId: 'project1',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      // Mock the arguments object after parsing
      (GetWikisSchema.parse as jest.Mock).mockReturnValue({
        projectId: 'project1',
      });

      const response = await handleWikisRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(mockWikis);
      expect(getWikis).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'project1',
        }),
      );
    });

    it('should handle get_wiki_page request', async () => {
      const mockWikiContent = '# Wiki Page\n\nThis is a wiki page content.';
      (getWikiPage as jest.Mock).mockResolvedValue(mockWikiContent);

      const request = {
        params: {
          name: 'get_wiki_page',
          arguments: {
            projectId: 'project1',
            wikiId: 'wiki1',
            pagePath: '/Home',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      // Mock the arguments object after parsing
      (GetWikiPageSchema.parse as jest.Mock).mockReturnValue({
        projectId: 'project1',
        wikiId: 'wiki1',
        pagePath: '/Home',
      });

      const response = await handleWikisRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(response.content[0].text as string).toEqual(mockWikiContent);
      expect(getWikiPage).toHaveBeenCalledWith(
        expect.objectContaining({
          projectId: 'project1',
          wikiId: 'wiki1',
          pagePath: '/Home',
        }),
      );
    });

    it('should handle create_wiki request', async () => {
      const mockWiki = { id: 'wiki1', name: 'New Wiki' };
      (createWiki as jest.Mock).mockResolvedValue(mockWiki);

      const request = {
        params: {
          name: 'create_wiki',
          arguments: {
            projectId: 'project1',
            name: 'New Wiki',
            type: WikiType.ProjectWiki,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      // Mock the arguments object after parsing
      (CreateWikiSchema.parse as jest.Mock).mockReturnValue({
        projectId: 'project1',
        name: 'New Wiki',
        type: WikiType.ProjectWiki,
        mappedPath: null, // Required field in the schema
      });

      const response = await handleWikisRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(mockWiki);
      expect(createWiki).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'project1',
          name: 'New Wiki',
          type: WikiType.ProjectWiki,
        }),
      );
    });

    it('should handle update_wiki_page request', async () => {
      const mockUpdateResult = { id: 'page1', content: 'Updated content' };
      (updateWikiPage as jest.Mock).mockResolvedValue(mockUpdateResult);

      const request = {
        params: {
          name: 'update_wiki_page',
          arguments: {
            projectId: 'project1',
            wikiId: 'wiki1',
            pagePath: '/Home',
            content: 'Updated content',
            comment: 'Update home page',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      // Mock the arguments object after parsing
      (UpdateWikiPageSchema.parse as jest.Mock).mockReturnValue({
        projectId: 'project1',
        wikiId: 'wiki1',
        pagePath: '/Home',
        content: 'Updated content',
        comment: 'Update home page',
      });

      const response = await handleWikisRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockUpdateResult,
      );
      expect(updateWikiPage).toHaveBeenCalledWith(
        expect.objectContaining({
          projectId: 'project1',
          wikiId: 'wiki1',
          pagePath: '/Home',
          content: 'Updated content',
          comment: 'Update home page',
        }),
      );
    });

    it('should throw error for unknown tool', async () => {
      const request = {
        params: {
          name: 'unknown_tool',
          arguments: {},
        },
        method: 'tools/call',
      } as CallToolRequest;

      await expect(handleWikisRequest(mockConnection, request)).rejects.toThrow(
        'Unknown wikis tool',
      );
    });

    it('should propagate errors from wiki functions', async () => {
      const mockError = new Error('Test error');
      (getWikis as jest.Mock).mockRejectedValue(mockError);

      const request = {
        params: {
          name: 'get_wikis',
          arguments: {
            projectId: 'project1',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      // Mock the arguments object after parsing
      (GetWikisSchema.parse as jest.Mock).mockReturnValue({
        projectId: 'project1',
      });

      await expect(handleWikisRequest(mockConnection, request)).rejects.toThrow(
        mockError,
      );
    });
  });
});

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { addPullRequestComment } from './feature';
import {
  Comment,
  CommentThreadStatus,
  CommentType,
  GitPullRequestCommentThread,
} from 'azure-devops-node-api/interfaces/GitInterfaces';

describe('addPullRequestComment', () => {
  afterEach(() => {
    jest.resetAllMocks();
  });

  test('should add a comment to an existing thread successfully', async () => {
    // Mock data for a new comment
    const mockComment: Comment = {
      id: 101,
      content: 'This is a reply comment',
      commentType: CommentType.Text,
      author: {
        displayName: 'Test User',
        id: 'test-user-id',
      },
      publishedDate: new Date(),
    };

    // Setup mock connection
    const mockGitApi = {
      createComment: jest.fn().mockResolvedValue(mockComment),
      createThread: jest.fn(),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const pullRequestId = 123;
    const threadId = 456;
    const options = {
      projectId,
      repositoryId,
      pullRequestId,
      threadId,
      content: 'This is a reply comment',
    };

    const result = await addPullRequestComment(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      pullRequestId,
      options,
    );

    // Verify results (with transformed commentType)
    expect(result).toEqual({
      comment: {
        ...mockComment,
        commentType: 'text', // Transform enum to string
      },
    });
    expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
    expect(mockGitApi.createComment).toHaveBeenCalledTimes(1);
    expect(mockGitApi.createComment).toHaveBeenCalledWith(
      expect.objectContaining({ content: 'This is a reply comment' }),
      repositoryId,
      pullRequestId,
      threadId,
      projectId,
    );
    expect(mockGitApi.createThread).not.toHaveBeenCalled();
  });

  test('should create a new thread with a comment successfully', async () => {
    // Mock data for a new thread with comment
    const mockComment: Comment = {
      id: 100,
      content: 'This is a new comment',
      commentType: CommentType.Text,
      author: {
        displayName: 'Test User',
        id: 'test-user-id',
      },
      publishedDate: new Date(),
    };

    const mockThread: GitPullRequestCommentThread = {
      id: 789,
      comments: [mockComment],
      status: CommentThreadStatus.Active,
    };

    // Setup mock connection
    const mockGitApi = {
      createComment: jest.fn(),
      createThread: jest.fn().mockResolvedValue(mockThread),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const pullRequestId = 123;
    const options = {
      projectId,
      repositoryId,
      pullRequestId,
      content: 'This is a new comment',
      status: 'active' as const,
    };

    const result = await addPullRequestComment(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      pullRequestId,
      options,
    );

    // Verify results
    expect(result).toEqual({
      comment: {
        ...mockComment,
        commentType: 'text',
      },
      thread: {
        ...mockThread,
        status: 'active',
        comments: mockThread.comments?.map((comment) => ({
          ...comment,
          commentType: 'text',
        })),
      },
    });
    expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
    expect(mockGitApi.createThread).toHaveBeenCalledTimes(1);
    expect(mockGitApi.createThread).toHaveBeenCalledWith(
      expect.objectContaining({
        comments: [
          expect.objectContaining({ content: 'This is a new comment' }),
        ],
        status: CommentThreadStatus.Active,
      }),
      repositoryId,
      pullRequestId,
      projectId,
    );
    expect(mockGitApi.createComment).not.toHaveBeenCalled();
  });

  test('should create a new thread on a file with line number', async () => {
    // Mock data for a new thread with comment on file
    const mockComment: Comment = {
      id: 100,
      content: 'This code needs improvement',
      commentType: CommentType.Text,
      author: {
        displayName: 'Test User',
        id: 'test-user-id',
      },
      publishedDate: new Date(),
    };

    const mockThread: GitPullRequestCommentThread = {
      id: 789,
      status: CommentThreadStatus.Active, // Add missing status
      comments: [mockComment],
      threadContext: {
        filePath: '/src/app.ts',
        rightFileStart: {
          line: 42,
          offset: 1,
        },
        rightFileEnd: {
          line: 42,
          offset: 1,
        },
      },
    };

    // Setup mock connection
    const mockGitApi = {
      createComment: jest.fn(),
      createThread: jest.fn().mockResolvedValue(mockThread),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const pullRequestId = 123;
    const options = {
      projectId,
      repositoryId,
      pullRequestId,
      content: 'This code needs improvement',
      filePath: '/src/app.ts',
      lineNumber: 42,
    };

    const result = await addPullRequestComment(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      pullRequestId,
      options,
    );

    // Verify results
    expect(result).toEqual({
      comment: {
        ...mockComment,
        commentType: 'text',
      },
      thread: {
        ...mockThread,
        status: 'active',
        comments: mockThread.comments?.map((comment) => ({
          ...comment,
          commentType: 'text',
        })),
      },
    });
    expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
    expect(mockGitApi.createThread).toHaveBeenCalledTimes(1);
    expect(mockGitApi.createThread).toHaveBeenCalledWith(
      expect.objectContaining({
        comments: [
          expect.objectContaining({ content: 'This code needs improvement' }),
        ],
        threadContext: expect.objectContaining({
          filePath: '/src/app.ts',
          rightFileStart: expect.objectContaining({ line: 42 }),
          rightFileEnd: expect.objectContaining({ line: 42 }),
        }),
      }),
      repositoryId,
      pullRequestId,
      projectId,
    );
    expect(mockGitApi.createComment).not.toHaveBeenCalled();
  });

  test('should handle error when API call fails', async () => {
    // Setup mock connection with error
    const errorMessage = 'API error';
    const mockGitApi = {
      createComment: jest.fn().mockRejectedValue(new Error(errorMessage)),
      createThread: jest.fn(),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const pullRequestId = 123;
    const threadId = 456;
    const options = {
      projectId,
      repositoryId,
      pullRequestId,
      threadId,
      content: 'This is a reply comment',
    };

    // Verify error handling
    await expect(
      addPullRequestComment(
        mockConnection as WebApi,
        projectId,
        repositoryId,
        pullRequestId,
        options,
      ),
    ).rejects.toThrow(`Failed to add pull request comment: ${errorMessage}`);
  });
});

```

--------------------------------------------------------------------------------
/project-management/troubleshooter.xml:
--------------------------------------------------------------------------------

```
<TroubleshootingGuide>
    <Flowchart>
        graph TD
        A[Start: Identify Issue] --> B[Gather Information]
        B --> C[Form Hypotheses]
        C --> D[Test Hypotheses]
        D -->|Issue Resolved?| E[Implement Solution]
        D -->|Issue Persists| B
        E --> F[Verify Fix]
        F -->|Success| G[Document &amp; Conclude]
        F -->|Failure| B
    </Flowchart>
    <Introduction>
        Troubleshooting is an essential skill that remains vital across all fields, from software
        development to mechanical engineering and household repairs. It’s the art of systematically
        identifying and resolving problems, a competency that never goes obsolete due to its
        universal relevance. Whether you’re debugging a crashing application, fixing a car that
        won’t start, or repairing a leaky faucet, troubleshooting empowers you to tackle challenges
        methodically. This guide provides a framework that adapts to any domain, highlighting the
        importance of a structured approach and a problem-solving mindset.
    </Introduction>
    <Preparation>
        Preparation is the foundation of effective troubleshooting. Before addressing any issue,
        take these steps:
        1. **Gather Necessary Tools:** Equip yourself with the right resources—debugging software
        for coding, wrenches and multimeters for machinery, or a toolkit with pliers and tape for
        home repairs.
        2. **Understand the System:** Study the system or device involved. Review code
        documentation, machine schematics, or appliance manuals to grasp how it should function.
        3. **Ensure Safety:** Prioritize safety by disconnecting power, wearing protective gear, or
        shutting off water supplies as needed.
        4. **Document the Initial State:** Note symptoms, error messages, or unusual behaviors
        (e.g., a software error code, a grinding noise from an engine, or water pooling under a
        sink) to establish a baseline.
        Proper preparation minimizes guesswork and sets the stage for efficient problem-solving.
    </Preparation>
    <Diagnosis>
        Diagnosis is the core of troubleshooting, requiring a systematic approach to uncover the
        root cause:
        1. **Gather Information:** Collect data through observation or tools—check software logs,
        listen for mechanical noises, or inspect pipes for leaks.
        2. **Form Hypotheses:** Develop theories about the cause based on evidence. For a software
        bug, suspect a recent code change; for a car, consider a dead battery; for a leak, think of
        a loose seal.
        3. **Test Hypotheses:** Conduct targeted tests—run a software debug session, measure battery
        voltage with a multimeter, or tighten a pipe fitting and check for drips.
        4. **Analyze Results:** Assess test outcomes to confirm or adjust your hypotheses. If the
        issue persists, gather more data and refine your theories.
        For example, in software, replicate a crash and trace it to a faulty loop; in mechanical
        engineering, test a pump after hearing a whine; in household repairs, turn on water to
        locate a drip’s source. This process is iterative—loop back as needed until the problem is
        clear.
    </Diagnosis>
    <SolutionImplementation>
        With the cause identified, implement a solution methodically:
        1. **Prioritize Fixes:** Address critical issues first—fix a server outage before a minor UI
        glitch, replace a broken engine belt before tuning performance, or stop a major leak before
        patching a crack.
        2. **Apply the Solution:** Execute the fix—patch the code and deploy it, install a new part,
        or replace a worn washer. Follow best practices or guidelines specific to the domain.
        3. **Test the Solution:** Verify the fix works—run the software, start the engine, or turn
        on the tap to ensure functionality.
        4. **Document Changes:** Record actions taken, like code updates in a changelog, parts
        swapped in a maintenance log, or repair steps in a notebook, for future reference.
        Examples include deploying a software update and checking for crashes, replacing a car
        alternator and testing the charge, or sealing a pipe and ensuring no leaks remain. Precision
        here prevents new issues.
    </SolutionImplementation>
    <Verification>
        Verification confirms the issue is resolved and the system is stable:
        1. **Perform Functional Tests:** Run the system normally—execute software features, drive
        the car, or use the repaired appliance.
        2. **Check for Side Effects:** Look for unintended outcomes, like new software errors,
        engine vibrations, or damp spots near a fix.
        3. **Monitor Over Time:** Observe performance longer-term—watch software logs for a day, run
        machinery through cycles, or check a pipe after hours of use.
        4. **Get User Feedback:** If applicable, ask users (e.g., software testers, car owners, or
        household members) to confirm the problem is gone.
        For instance, monitor a web app post-fix for uptime, test a repaired tractor under load, or
        ensure a faucet stays dry overnight. Thorough verification ensures lasting success.
    </Verification>
    <CommonMistakes>
        Avoid these pitfalls to troubleshoot effectively:
        1. **Jumping to Conclusions:** Assuming a software crash is a server issue without logs, or
        replacing an engine part without testing, wastes time—always validate hypotheses.
        2. **Neglecting Documentation:** Skipping notes on code changes or repair steps complicates
        future fixes—keep detailed records.
        3. **Overlooking Simple Solutions:** A reboot might fix a software glitch, or a loose bolt
        could be the mechanical issue—check the obvious first.
        4. **Insufficient Testing:** Deploying a patch without full tests, or assuming a pipe is
        fixed after a quick look, risks recurrence—test rigorously.
        5. **Ignoring Safety:** Debugging live circuits or repairing plumbing without shutoffs
        invites danger—prioritize safety always.
        Awareness of these errors keeps your process on track.
    </CommonMistakes>
    <MindsetTips>
        Cultivate this mindset for troubleshooting success:
        1. **Patience:** Problems may resist quick fixes—stay calm when a bug eludes you or a repair
        takes hours.
        2. **Attention to Detail:** Notice subtle clues—a log timestamp, a faint hum, or a drip
        pattern can crack the case.
        3. **Persistence:** If a fix fails, keep testing—don’t abandon a software trace or a machine
        teardown midstream.
        4. **Open-Mindedness:** A bug might stem from an overlooked module, or a leak from an
        unexpected joint—stay flexible.
        5. **Learning Orientation:** Each challenge teaches something—log a new coding trick, a
        mechanical quirk, or a repair tip.
        6. **Collaboration:** Seek input—a colleague might spot a code flaw or a neighbor recall a
        similar fix.
        This mindset turns obstacles into opportunities.
    </MindsetTips>
    <Conclusion>
        Troubleshooting is a timeless skill that equips you to solve problems anywhere—from
        codebases to engines to homes. This guide’s systematic approach—preparing diligently,
        diagnosing precisely, implementing thoughtfully, and verifying completely—ensures success
        across domains. Avoiding common mistakes and embracing a resilient mindset amplify your
        effectiveness. Mastering troubleshooting not only boosts professional prowess but also
        fosters everyday resourcefulness, making it a skill worth honing for life.
    </Conclusion>
</TroubleshootingGuide>
```

--------------------------------------------------------------------------------
/project-management/startup.xml:
--------------------------------------------------------------------------------

```
<AiTaskAgent>
  <GlobalRule alwaysApply="true">If an ANY point you get stuck, review troubleshooter.xml to help you troubleshoot the problem.</GlobalRule>
  <GlobalRule alwaysApply="true">All new code creation should ALWAYS follow tdd-cycle.xml</GlobalRule>
  <GlobalRule alwaysApply="true">Tasks in the GitHub project board at https://github.com/users/Tiberriver256/projects/1 are sorted in order of priority - ALWAYS pick the task from the top of the backlog column.</GlobalRule>
  <GlobalRule alwaysApply="true">Always use the GitHub CLI (gh) for project and issue management. If documentation is needed, use browser_navigate to access the documentation. Always use a markdown file for writing/updating issues rather than trying to work with cli args.</GlobalRule>
  <GlobalRule alwaysApply="true">There is a strict WIP limit of 1. If any issue is in the Research, Implementation, or In Review status, that issue MUST be completed before starting a new one from Backlog.</GlobalRule>
  <GlobalRule alwaysApply="true">We are always operating as the GitHub user 'Tiberriver256'. All issues must be assigned to 'Tiberriver256' before starting work on them.</GlobalRule>
  <GlobalRule alwaysApply="true">To update a project item status, first get the project ID with `gh project list --owner Tiberriver256 --format json`, then get the item ID with `gh project item-list [project-id] --format json`, and finally update the status with `gh project item-edit --id [item-id] --project-id [project-id] --field-id PVTSSF_lAHOAGqmtM4A2BrBzgrZoeI --single-select-option-id [status-id]`. Status IDs are: Backlog (f75ad846), Research (61e4505c), Implementation (47fc9ee4), In review (df73e18b), Done (98236657).</GlobalRule>
  <GlobalRule alwaysApply="true">To create a GitHub issue: 1) Create a markdown file for the issue body (e.g., `issue_body.md`), 2) Use `gh issue create --title "Issue Title" --body-file issue_body.md --label "enhancement"` to create the issue, 3) Assign it with `gh issue edit [issue-number] --add-assignee Tiberriver256`, 4) Add status label with `gh issue edit [issue-number] --add-label "status:research"`, 5) Add to project with `gh project item-add [project-id] --owner Tiberriver256 --url [issue-url]`, and 6) Update status in project using the project item-edit command as described above.</GlobalRule>
  <InitialSetup order="1">
    <Step order="1">Read the dream team documentation at project-management/planning/the-dream-team.md to understand the team structure and roles</Step>
    <Step order="2">Read all files in the project-management/planning directory to understand the project architecture, features, and structure</Step>
    <Step order="3">Check if there is any issue in the GitHub project board at https://github.com/users/Tiberriver256/projects/1 with a status of "Research", "Implementation", or "In Review". Use 'gh project item-list' to check the current issues and their status.</Step>
    <Step order="4">
      If there is any issue in "Research", "Implementation", or "In Review" status, ensure it is assigned to 'Tiberriver256' and work on that issue, moving directly into the appropriate phase of TaskWorkflow.
      If not, take the FIRST issue from the top of the "Backlog" status in the project board at https://github.com/users/Tiberriver256/projects/1, assign it to 'Tiberriver256', and update its status to "Research". 
      Remember that issues are sorted by priority with most important at the top. Add a comment with your implementation approach and planned sub-tasks if needed. Use the GitHub CLI (gh) for all project and issue management.
    </Step>
    <Step order="5">Create a new branch for the current task, branching from the latest main branch. Use a descriptive name for the branch, related to the task, by running ./create_branch.sh &lt;branch_name&gt;.</Step>
    <Step order="6">Read tdd-cycle.xml to understand the TDD cycle.</Step>
    <Step order="7">Read all files in the docs/testing directory to understand the testing strategy.</Step>
    <Step order="8">Start the research phase of TaskWorkflow.</Step>
  </InitialSetup>
  
  <TaskWorkflow order="2">
    <Phase name="Research" order="1">
      <Step order="1">Make sure the issue is assigned to 'Tiberriver256' and its status is set to "Research" in the GitHub project board.</Step>
      <Step order="2">Research the selected GitHub issue thoroughly</Step>
      <Step order="3">Create notes in a comment on the GitHub issue about your approach. Use the GitHub CLI (gh) for interacting with issues.</Step>
      <Step order="4">Break down the task into sub-tasks only if necessary (prefer simplicity)</Step>
      <Step order="5">If the task is straightforward, keep it as a single task</Step>
    </Phase>
    
    <Phase name="Planning" order="2">
      <STOPPING_POINT order="1">Present your sub-tasks (if any) and approach for approval</STOPPING_POINT>
    </Phase>
    
    <Phase name="Implementation" order="3">
      <Step order="1">Update the issue status to "Implementation" in the GitHub project board, ensuring it remains assigned to 'Tiberriver256'.</Step>
      <Step order="2">Assume the role and persona of the team member assigned to the task</Step>
      <Step order="3">If multiple roles are involved, simulate pair/mob programming</Step>
      <Step order="4">Use Test-Driven Development for all coding tasks</Step>
      <Step order="5">Create any necessary readme.md files for documentation or reference</Step>
    </Phase>
    
    <Phase name="Completion" order="4">
      <Step order="1">Create a pull request and update the issue status to "In Review" in the GitHub project board, ensuring it remains assigned to 'Tiberriver256'.</Step>
      <STOPPING_POINT order="2">Present your work for review</STOPPING_POINT>
      <Step order="3">Address any feedback, and present for re-review; Continue in this manner until approved</Step>
      <Step order="4">When the task is approved, run ./finish_task.sh "PR Title" "PR Description" to commit, push, and update the PR for the repository at https://github.com/Tiberriver256/mcp-server-azure-devops</Step>
      <Step order="5">After the PR is merged, update the issue status to "Done" and close the GitHub issue with an appropriate comment summarizing the work done. Use the GitHub CLI (gh) to close issues.</Step>
      <Step order="6">Wait for feedback before starting a new task</Step>
    </Phase>
  </TaskWorkflow>
  
  <WorkingPrinciples>
    <Principle>Use the tree command when exploring directory structures</Principle>
    <Principle>Follow KISS (Keep It Stupid Simple) and YAGNI (You Aren't Gonna Need It) principles</Principle>
    <Principle>Focus on delivery rather than over-engineering or gold-plating features</Principle>
    <Principle>Implement Test-Driven Development for all code</Principle>
    <Principle>Use the GitHub CLI (gh) for any GitHub-related tasks including project and issue management. Documentation is available at:
      - GitHub Projects CLI: https://cli.github.com/manual/gh_project
      - GitHub Issues CLI: https://cli.github.com/manual/gh_issue</Principle>
    <Principle>If GitHub CLI documentation is needed, use browser_navigate to access documentation</Principle>
    <Principle>Use Puppeteer if web browsing is required</Principle>
    <Principle>If any task is unclear, stop and ask for clarification before proceeding</Principle>
    <Principle>Always take tasks from the top of the GitHub project backlog column at https://github.com/users/Tiberriver256/projects/1 as they are sorted in priority order</Principle>
    <Principle>Strictly adhere to the WIP limit of 1 - only one issue should be in Research, Implementation, or In Review status at any time</Principle>
    <Principle>Move issues through the status workflow: Backlog → Research → Implementation → In Review → Done</Principle>
    <Principle>All work is performed as the GitHub user 'Tiberriver256'. Ensure all issues you work on are assigned to this user.</Principle>
  </WorkingPrinciples>
</AiTaskAgent>

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { listPullRequests } from './feature';
import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';

describe('listPullRequests', () => {
  afterEach(() => {
    jest.resetAllMocks();
  });

  test('should return pull requests successfully with pagination metadata', async () => {
    // Mock data
    const mockPullRequests = [
      {
        pullRequestId: 1,
        title: 'Test PR 1',
        description: 'Test PR description 1',
      },
      {
        pullRequestId: 2,
        title: 'Test PR 2',
        description: 'Test PR description 2',
      },
    ];

    // Setup mock connection
    const mockGitApi = {
      getPullRequests: jest.fn().mockResolvedValue(mockPullRequests),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const options = {
      projectId,
      repositoryId,
      status: 'active' as const,
      top: 10,
    };

    const result = await listPullRequests(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      options,
    );

    // Verify results
    expect(result).toEqual({
      count: 2,
      value: mockPullRequests,
      hasMoreResults: false,
      warning: undefined,
    });
    expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
    expect(mockGitApi.getPullRequests).toHaveBeenCalledTimes(1);
    expect(mockGitApi.getPullRequests).toHaveBeenCalledWith(
      repositoryId,
      { status: PullRequestStatus.Active },
      projectId,
      undefined, // maxCommentLength
      0, // skip
      10, // top
    );
  });

  test('should return single pull request when pullRequestId is provided', async () => {
    const mockPullRequest = {
      pullRequestId: 42,
      title: 'Specific PR',
    };

    const mockGitApi = {
      getPullRequest: jest.fn().mockResolvedValue(mockPullRequest),
      getPullRequests: jest.fn(),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const options = {
      projectId,
      repositoryId,
      pullRequestId: 42,
    };

    const result = await listPullRequests(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      options,
    );

    expect(result).toEqual({
      count: 1,
      value: [mockPullRequest],
      hasMoreResults: false,
      warning: undefined,
    });
    expect(mockGitApi.getPullRequest).toHaveBeenCalledWith(
      repositoryId,
      42,
      projectId,
    );
    expect(mockGitApi.getPullRequests).not.toHaveBeenCalled();
  });

  test('should return empty array when no pull requests exist', async () => {
    // Setup mock connection
    const mockGitApi = {
      getPullRequests: jest.fn().mockResolvedValue(null),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const options = { projectId, repositoryId };

    const result = await listPullRequests(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      options,
    );

    // Verify results
    expect(result).toEqual({
      count: 0,
      value: [],
      hasMoreResults: false,
      warning: undefined,
    });
    expect(mockConnection.getGitApi).toHaveBeenCalledTimes(1);
    expect(mockGitApi.getPullRequests).toHaveBeenCalledTimes(1);
  });

  test('should handle all filter options correctly', async () => {
    // Setup mock connection
    const mockGitApi = {
      getPullRequests: jest.fn().mockResolvedValue([]),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call with all options
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const options = {
      projectId,
      repositoryId,
      status: 'completed' as const,
      creatorId: 'a8a8a8a8-a8a8-a8a8-a8a8-a8a8a8a8a8a8',
      reviewerId: 'b9b9b9b9-b9b9-b9b9-b9b9-b9b9b9b9b9b9',
      sourceRefName: 'refs/heads/source-branch',
      targetRefName: 'refs/heads/target-branch',
      top: 5,
      skip: 10,
    };

    await listPullRequests(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      options,
    );

    // Verify the search criteria was constructed correctly
    expect(mockGitApi.getPullRequests).toHaveBeenCalledWith(
      repositoryId,
      {
        status: PullRequestStatus.Completed,
        creatorId: 'a8a8a8a8-a8a8-a8a8-a8a8-a8a8a8a8a8a8',
        reviewerId: 'b9b9b9b9-b9b9-b9b9-b9b9-b9b9b9b9b9b9',
        sourceRefName: 'refs/heads/source-branch',
        targetRefName: 'refs/heads/target-branch',
      },
      projectId,
      undefined, // maxCommentLength
      10, // skip
      5, // top
    );
  });

  test('should throw error when API call fails', async () => {
    // Setup mock connection
    const errorMessage = 'API error';
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getPullRequests: jest.fn().mockRejectedValue(new Error(errorMessage)),
      })),
    };

    // Call the function with test parameters
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const options = { projectId, repositoryId };

    // Verify error handling
    await expect(
      listPullRequests(
        mockConnection as WebApi,
        projectId,
        repositoryId,
        options,
      ),
    ).rejects.toThrow(`Failed to list pull requests: ${errorMessage}`);
  });

  test('should use default pagination values when not provided', async () => {
    // Mock data
    const mockPullRequests = [
      { pullRequestId: 1, title: 'Test PR 1' },
      { pullRequestId: 2, title: 'Test PR 2' },
    ];

    // Setup mock connection
    const mockGitApi = {
      getPullRequests: jest.fn().mockResolvedValue(mockPullRequests),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call the function with minimal parameters (no top or skip)
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const options = { projectId, repositoryId };

    const result = await listPullRequests(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      options,
    );

    // Verify default values were used
    expect(mockGitApi.getPullRequests).toHaveBeenCalledWith(
      repositoryId,
      {},
      projectId,
      undefined, // maxCommentLength
      0, // default skip
      10, // default top
    );

    expect(result.count).toBe(2);
    expect(result.value).toEqual(mockPullRequests);
  });

  test('should add warning when hasMoreResults is true', async () => {
    // Create exactly 10 mock pull requests to trigger hasMoreResults
    const mockPullRequests = Array(10)
      .fill(0)
      .map((_, i) => ({
        pullRequestId: i + 1,
        title: `Test PR ${i + 1}`,
      }));

    // Setup mock connection
    const mockGitApi = {
      getPullRequests: jest.fn().mockResolvedValue(mockPullRequests),
    };

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    };

    // Call with top=10 to match the number of results
    const projectId = 'test-project';
    const repositoryId = 'test-repo';
    const options = {
      projectId,
      repositoryId,
      top: 10,
      skip: 5,
    };

    const result = await listPullRequests(
      mockConnection as WebApi,
      projectId,
      repositoryId,
      options,
    );

    // Verify hasMoreResults is true and warning is set
    expect(result.hasMoreResults).toBe(true);
    expect(result.warning).toBe(
      "Results limited to 10 items. Use 'skip: 15' to get the next page.",
    );
  });
});

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import axios from 'axios';
import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity';
import {
  AzureDevOpsError,
  AzureDevOpsResourceNotFoundError,
  AzureDevOpsValidationError,
  AzureDevOpsPermissionError,
  AzureDevOpsAuthenticationError,
} from '../../../shared/errors';
import {
  SearchCodeOptions,
  CodeSearchRequest,
  CodeSearchResponse,
  CodeSearchResult,
} from '../types';
import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';

/**
 * Search for code in Azure DevOps repositories
 *
 * @param connection The Azure DevOps WebApi connection
 * @param options Parameters for searching code
 * @returns Search results with optional file content
 */
export async function searchCode(
  connection: WebApi,
  options: SearchCodeOptions,
): Promise<CodeSearchResponse> {
  try {
    // When includeContent is true, limit results to prevent timeouts
    const top = options.includeContent
      ? Math.min(options.top || 10, 10)
      : options.top;

    // Get the project ID (either provided or default)
    const projectId =
      options.projectId || process.env.AZURE_DEVOPS_DEFAULT_PROJECT;

    if (!projectId) {
      throw new AzureDevOpsValidationError(
        'Project ID is required. Either provide a projectId or set the AZURE_DEVOPS_DEFAULT_PROJECT environment variable.',
      );
    }

    // Prepare the search request
    const searchRequest: CodeSearchRequest = {
      searchText: options.searchText,
      $skip: options.skip,
      $top: top, // Use limited top value when includeContent is true
      filters: {
        Project: [projectId],
        ...(options.filters || {}),
      },
      includeFacets: true,
      includeSnippet: options.includeSnippet,
    };

    // Get the authorization header from the connection
    const authHeader = await getAuthorizationHeader();

    // Extract organization from the connection URL
    const { organization } = extractOrgFromUrl(connection);

    // Make the search API request with the project ID
    const searchUrl = `https://almsearch.dev.azure.com/${organization}/${projectId}/_apis/search/codesearchresults?api-version=7.1`;

    const searchResponse = await axios.post<CodeSearchResponse>(
      searchUrl,
      searchRequest,
      {
        headers: {
          Authorization: authHeader,
          'Content-Type': 'application/json',
        },
      },
    );

    const results = searchResponse.data;

    // If includeContent is true, fetch the content for each result
    if (options.includeContent && results.results.length > 0) {
      await enrichResultsWithContent(connection, results.results);
    }

    return results;
  } catch (error) {
    if (error instanceof AzureDevOpsError) {
      throw error;
    }

    if (axios.isAxiosError(error)) {
      const status = error.response?.status;
      if (status === 404) {
        throw new AzureDevOpsResourceNotFoundError(
          'Repository or project not found',
          { cause: error },
        );
      }
      if (status === 400) {
        throw new AzureDevOpsValidationError(
          'Invalid search parameters',
          error.response?.data,
          { cause: error },
        );
      }
      if (status === 401) {
        throw new AzureDevOpsAuthenticationError('Authentication failed', {
          cause: error,
        });
      }
      if (status === 403) {
        throw new AzureDevOpsPermissionError(
          'Permission denied to access repository',
          { cause: error },
        );
      }
    }

    throw new AzureDevOpsError('Failed to search code', { cause: error });
  }
}

/**
 * Extract organization from the connection URL
 *
 * @param connection The Azure DevOps WebApi connection
 * @returns The organization
 */
function extractOrgFromUrl(connection: WebApi): { organization: string } {
  // Extract organization from the connection URL
  const url = connection.serverUrl;
  const match = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)/);
  const organization = match ? match[1] : '';

  if (!organization) {
    throw new AzureDevOpsValidationError(
      'Could not extract organization from connection URL',
    );
  }

  return {
    organization,
  };
}

/**
 * Get the authorization header from the connection
 *
 * @returns The authorization header
 */
async function getAuthorizationHeader(): Promise<string> {
  try {
    // For PAT authentication, we can construct the header directly
    if (
      process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'pat' &&
      process.env.AZURE_DEVOPS_PAT
    ) {
      // For PAT auth, we can construct the Basic auth header directly
      const token = process.env.AZURE_DEVOPS_PAT;
      const base64Token = Buffer.from(`:${token}`).toString('base64');
      return `Basic ${base64Token}`;
    }

    // For Azure Identity / Azure CLI auth, we need to get a token
    // using the Azure DevOps resource ID
    // Choose the appropriate credential based on auth method
    const credential =
      process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'azure-cli'
        ? new AzureCliCredential()
        : new DefaultAzureCredential();

    // Azure DevOps resource ID for token acquisition
    const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798';

    // Get token for Azure DevOps
    const token = await credential.getToken(
      `${AZURE_DEVOPS_RESOURCE_ID}/.default`,
    );

    if (!token || !token.token) {
      throw new Error('Failed to acquire token for Azure DevOps');
    }

    return `Bearer ${token.token}`;
  } catch (error) {
    throw new AzureDevOpsValidationError(
      `Failed to get authorization header: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

/**
 * Enrich search results with file content
 *
 * @param connection The Azure DevOps WebApi connection
 * @param results The search results to enrich
 */
async function enrichResultsWithContent(
  connection: WebApi,
  results: CodeSearchResult[],
): Promise<void> {
  try {
    const gitApi = await connection.getGitApi();

    // Process each result in parallel
    await Promise.all(
      results.map(async (result) => {
        try {
          // Get the file content using the Git API
          // Pass only the required parameters to avoid the "path" and "scopePath" conflict
          const contentStream = await gitApi.getItemContent(
            result.repository.id,
            result.path,
            result.project.name,
            undefined, // No version descriptor object
            undefined, // No recursion level
            undefined, // Don't include content metadata
            undefined, // No latest processed change
            false, // Don't download
            {
              version: result.versions[0]?.changeId,
              versionType: GitVersionType.Commit,
            }, // Version descriptor
            true, // Include content
          );

          // Convert the stream to a string and store it in the result
          if (contentStream) {
            // Since getItemContent always returns NodeJS.ReadableStream, we need to read the stream
            const chunks: Buffer[] = [];

            // Listen for data events to collect chunks
            contentStream.on('data', (chunk) => {
              chunks.push(Buffer.from(chunk));
            });

            // Use a promise to wait for the stream to finish
            result.content = await new Promise<string>((resolve, reject) => {
              contentStream.on('end', () => {
                // Concatenate all chunks and convert to string
                const buffer = Buffer.concat(chunks);
                resolve(buffer.toString('utf8'));
              });

              contentStream.on('error', (err) => {
                reject(err);
              });
            });
          }
        } catch (error) {
          // Log the error but don't fail the entire operation
          console.error(
            `Failed to fetch content for ${result.path}: ${error instanceof Error ? error.message : String(error)}`,
          );
        }
      }),
    );
  } catch (error) {
    // Log the error but don't fail the entire operation
    console.error(
      `Failed to enrich results with content: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

--------------------------------------------------------------------------------
/src/shared/api/client.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { ICoreApi } from 'azure-devops-node-api/CoreApi';
import { IGitApi } from 'azure-devops-node-api/GitApi';
import { IWorkItemTrackingApi } from 'azure-devops-node-api/WorkItemTrackingApi';
import { IBuildApi } from 'azure-devops-node-api/BuildApi';
import { ITestApi } from 'azure-devops-node-api/TestApi';
import { IReleaseApi } from 'azure-devops-node-api/ReleaseApi';
import { ITaskAgentApi } from 'azure-devops-node-api/TaskAgentApi';
import { ITaskApi } from 'azure-devops-node-api/TaskApi';
import { AzureDevOpsError, AzureDevOpsAuthenticationError } from '../errors';
import { AuthenticationMethod } from '../auth';
import { AzureDevOpsClient as SharedClient } from '../auth/client-factory';

export interface AzureDevOpsClientConfig {
  orgUrl: string;
  pat: string;
}

/**
 * Azure DevOps Client
 *
 * Provides access to Azure DevOps APIs
 */
export class AzureDevOpsClient {
  private config: AzureDevOpsClientConfig;
  private clientPromise: Promise<WebApi> | null = null;

  constructor(config: AzureDevOpsClientConfig) {
    this.config = config;
  }

  /**
   * Get the authenticated Azure DevOps client
   *
   * @returns The authenticated WebApi client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  private async getClient(): Promise<WebApi> {
    if (!this.clientPromise) {
      this.clientPromise = (async () => {
        try {
          const sharedClient = new SharedClient({
            method: AuthenticationMethod.PersonalAccessToken,
            organizationUrl: this.config.orgUrl,
            personalAccessToken: this.config.pat,
          });
          return await sharedClient.getWebApiClient();
        } catch (error) {
          // If it's already an AzureDevOpsError, rethrow it
          if (error instanceof AzureDevOpsError) {
            throw error;
          }
          // Otherwise, wrap it in an AzureDevOpsAuthenticationError
          throw new AzureDevOpsAuthenticationError(
            error instanceof Error
              ? `Authentication failed: ${error.message}`
              : 'Authentication failed: Unknown error',
          );
        }
      })();
    }
    return this.clientPromise;
  }

  /**
   * Check if the client is authenticated
   *
   * @returns True if the client is authenticated
   */
  public async isAuthenticated(): Promise<boolean> {
    try {
      const client = await this.getClient();
      return !!client;
    } catch {
      // Any error means we're not authenticated
      return false;
    }
  }

  /**
   * Get the Core API
   *
   * @returns The Core API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getCoreApi(): Promise<ICoreApi> {
    try {
      const client = await this.getClient();
      return await client.getCoreApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Core API: ${error.message}`
          : 'Failed to get Core API: Unknown error',
      );
    }
  }

  /**
   * Get the Git API
   *
   * @returns The Git API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getGitApi(): Promise<IGitApi> {
    try {
      const client = await this.getClient();
      return await client.getGitApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Git API: ${error.message}`
          : 'Failed to get Git API: Unknown error',
      );
    }
  }

  /**
   * Get the Work Item Tracking API
   *
   * @returns The Work Item Tracking API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getWorkItemTrackingApi(): Promise<IWorkItemTrackingApi> {
    try {
      const client = await this.getClient();
      return await client.getWorkItemTrackingApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Work Item Tracking API: ${error.message}`
          : 'Failed to get Work Item Tracking API: Unknown error',
      );
    }
  }

  /**
   * Get the Build API
   *
   * @returns The Build API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getBuildApi(): Promise<IBuildApi> {
    try {
      const client = await this.getClient();
      return await client.getBuildApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Build API: ${error.message}`
          : 'Failed to get Build API: Unknown error',
      );
    }
  }

  /**
   * Get the Test API
   *
   * @returns The Test API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getTestApi(): Promise<ITestApi> {
    try {
      const client = await this.getClient();
      return await client.getTestApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Test API: ${error.message}`
          : 'Failed to get Test API: Unknown error',
      );
    }
  }

  /**
   * Get the Release API
   *
   * @returns The Release API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getReleaseApi(): Promise<IReleaseApi> {
    try {
      const client = await this.getClient();
      return await client.getReleaseApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Release API: ${error.message}`
          : 'Failed to get Release API: Unknown error',
      );
    }
  }

  /**
   * Get the Task Agent API
   *
   * @returns The Task Agent API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getTaskAgentApi(): Promise<ITaskAgentApi> {
    try {
      const client = await this.getClient();
      return await client.getTaskAgentApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Task Agent API: ${error.message}`
          : 'Failed to get Task Agent API: Unknown error',
      );
    }
  }

  /**
   * Get the Task API
   *
   * @returns The Task API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getTaskApi(): Promise<ITaskApi> {
    try {
      const client = await this.getClient();
      return await client.getTaskApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Task API: ${error.message}`
          : 'Failed to get Task API: Unknown error',
      );
    }
  }
}

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { isPipelinesRequest, handlePipelinesRequest } from './index';
import { listPipelines } from './list-pipelines/feature';
import { getPipeline } from './get-pipeline/feature';
import { listPipelineRuns } from './list-pipeline-runs/feature';
import { getPipelineRun } from './get-pipeline-run/feature';
import { getPipelineTimeline } from './pipeline-timeline/feature';
import { getPipelineLog } from './get-pipeline-log/feature';
import { triggerPipeline } from './trigger-pipeline/feature';

jest.mock('./list-pipelines/feature');
jest.mock('./get-pipeline/feature');
jest.mock('./list-pipeline-runs/feature');
jest.mock('./get-pipeline-run/feature');
jest.mock('./pipeline-timeline/feature');
jest.mock('./get-pipeline-log/feature');
jest.mock('./trigger-pipeline/feature');

describe('Pipelines Request Handlers', () => {
  const mockConnection = {} as WebApi;

  describe('isPipelinesRequest', () => {
    it('should return true for pipelines requests', () => {
      const validTools = [
        'list_pipelines',
        'get_pipeline',
        'list_pipeline_runs',
        'get_pipeline_run',
        'pipeline_timeline',
        'get_pipeline_log',
        'trigger_pipeline',
      ];
      validTools.forEach((tool) => {
        const request = {
          params: { name: tool, arguments: {} },
          method: 'tools/call',
        } as CallToolRequest;
        expect(isPipelinesRequest(request)).toBe(true);
      });
    });

    it('should return false for non-pipelines requests', () => {
      const request = {
        params: { name: 'get_project', arguments: {} },
        method: 'tools/call',
      } as CallToolRequest;
      expect(isPipelinesRequest(request)).toBe(false);
    });
  });

  describe('handlePipelinesRequest', () => {
    it('should handle list_pipelines request', async () => {
      const mockPipelines = [
        { id: 1, name: 'Pipeline 1' },
        { id: 2, name: 'Pipeline 2' },
      ];

      (listPipelines as jest.Mock).mockResolvedValue(mockPipelines);

      const request = {
        params: {
          name: 'list_pipelines',
          arguments: {
            projectId: 'test-project',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handlePipelinesRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockPipelines,
      );
      expect(listPipelines).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'test-project',
        }),
      );
    });

    it('should handle get_pipeline request', async () => {
      const mockPipeline = { id: 1, name: 'Pipeline 1' };
      (getPipeline as jest.Mock).mockResolvedValue(mockPipeline);

      const request = {
        params: {
          name: 'get_pipeline',
          arguments: {
            projectId: 'test-project',
            pipelineId: 1,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handlePipelinesRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockPipeline,
      );
      expect(getPipeline).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'test-project',
          pipelineId: 1,
        }),
      );
    });

    it('should handle trigger_pipeline request', async () => {
      const mockRun = { id: 1, state: 'inProgress' };
      (triggerPipeline as jest.Mock).mockResolvedValue(mockRun);

      const request = {
        params: {
          name: 'trigger_pipeline',
          arguments: {
            projectId: 'test-project',
            pipelineId: 1,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handlePipelinesRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(mockRun);
      expect(triggerPipeline).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'test-project',
          pipelineId: 1,
        }),
      );
    });

    it('should handle list_pipeline_runs request', async () => {
      const mockRuns = { runs: [{ id: 1 }], continuationToken: 'next' };
      (listPipelineRuns as jest.Mock).mockResolvedValue(mockRuns);

      const request = {
        params: {
          name: 'list_pipeline_runs',
          arguments: {
            projectId: 'test-project',
            pipelineId: 99,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handlePipelinesRequest(mockConnection, request);
      expect(JSON.parse(response.content[0].text as string)).toEqual(mockRuns);
      expect(listPipelineRuns).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'test-project',
          pipelineId: 99,
        }),
      );
    });

    it('should handle get_pipeline_run request', async () => {
      const mockRun = { id: 123 };
      (getPipelineRun as jest.Mock).mockResolvedValue(mockRun);

      const request = {
        params: {
          name: 'get_pipeline_run',
          arguments: {
            projectId: 'test-project',
            runId: 123,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handlePipelinesRequest(mockConnection, request);
      expect(JSON.parse(response.content[0].text as string)).toEqual(mockRun);
      expect(getPipelineRun).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'test-project',
          runId: 123,
        }),
      );
    });

    it('should handle pipeline_timeline request', async () => {
      const mockTimeline = { records: [{ name: 'Stage 1' }] };
      (getPipelineTimeline as jest.Mock).mockResolvedValue(mockTimeline);

      const request = {
        params: {
          name: 'pipeline_timeline',
          arguments: {
            projectId: 'test-project',
            pipelineId: 99,
            runId: 321,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handlePipelinesRequest(mockConnection, request);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockTimeline,
      );
      expect(getPipelineTimeline).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'test-project',
          pipelineId: 99,
          runId: 321,
        }),
      );
    });

    it('should handle get_pipeline_log request', async () => {
      (getPipelineLog as jest.Mock).mockResolvedValue('log lines');

      const request = {
        params: {
          name: 'get_pipeline_log',
          arguments: {
            projectId: 'test-project',
            pipelineId: 99,
            runId: 321,
            logId: 7,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handlePipelinesRequest(mockConnection, request);
      expect(response.content[0].text).toBe('log lines');
      expect(getPipelineLog).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'test-project',
          pipelineId: 99,
          runId: 321,
          logId: 7,
        }),
      );
    });

    it('should throw error for unknown tool', async () => {
      const request = {
        params: {
          name: 'unknown_tool',
          arguments: {},
        },
        method: 'tools/call',
      } as CallToolRequest;

      await expect(
        handlePipelinesRequest(mockConnection, request),
      ).rejects.toThrow('Unknown pipelines tool');
    });

    it('should propagate errors from pipeline functions', async () => {
      const mockError = new Error('Test error');
      (listPipelines as jest.Mock).mockRejectedValue(mockError);

      const request = {
        params: {
          name: 'list_pipelines',
          arguments: {
            projectId: 'test-project',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      await expect(
        handlePipelinesRequest(mockConnection, request),
      ).rejects.toThrow(mockError);
    });
  });
});

```

--------------------------------------------------------------------------------
/src/server.spec.e2e.ts:
--------------------------------------------------------------------------------

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { spawn } from 'child_process';
import { join } from 'path';
import dotenv from 'dotenv';
import { Organization } from './features/organizations/types';
import fs from 'fs';

// Load environment variables from .env file
dotenv.config();

describe('Azure DevOps MCP Server E2E Tests', () => {
  let client: Client;
  let serverProcess: ReturnType<typeof spawn>;
  let transport: StdioClientTransport;
  let tempEnvFile: string | null = null;

  beforeAll(async () => {
    // Debug: Log environment variables
    console.error('E2E TEST ENVIRONMENT VARIABLES:');
    console.error(
      `AZURE_DEVOPS_ORG_URL: ${process.env.AZURE_DEVOPS_ORG_URL || 'NOT SET'}`,
    );
    console.error(
      `AZURE_DEVOPS_PAT: ${process.env.AZURE_DEVOPS_PAT ? 'SET (hidden value)' : 'NOT SET'}`,
    );
    console.error(
      `AZURE_DEVOPS_DEFAULT_PROJECT: ${process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'NOT SET'}`,
    );
    console.error(
      `AZURE_DEVOPS_AUTH_METHOD: ${process.env.AZURE_DEVOPS_AUTH_METHOD || 'NOT SET'}`,
    );

    // Start the MCP server process
    const serverPath = join(process.cwd(), 'dist', 'index.js');

    // Create a temporary .env file for testing if needed
    const orgUrl = process.env.AZURE_DEVOPS_ORG_URL || '';
    const pat = process.env.AZURE_DEVOPS_PAT || '';
    const defaultProject = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || '';
    const authMethod = process.env.AZURE_DEVOPS_AUTH_METHOD || 'pat';

    if (orgUrl) {
      // Create a temporary .env file for the test
      tempEnvFile = join(process.cwd(), '.env.e2e-test');

      const envFileContent = `
AZURE_DEVOPS_ORG_URL=${orgUrl}
AZURE_DEVOPS_PAT=${pat}
AZURE_DEVOPS_DEFAULT_PROJECT=${defaultProject}
AZURE_DEVOPS_AUTH_METHOD=${authMethod}
`;

      fs.writeFileSync(tempEnvFile, envFileContent);
      console.error(`Created temporary .env file at ${tempEnvFile}`);

      // Start server with explicit file path to the temp .env file
      serverProcess = spawn('node', ['-r', 'dotenv/config', serverPath], {
        env: {
          ...process.env,
          NODE_ENV: 'test',
          DOTENV_CONFIG_PATH: tempEnvFile,
        },
      });
    } else {
      throw new Error(
        'Cannot start server: AZURE_DEVOPS_ORG_URL is not set in the environment',
      );
    }

    // Capture server output for debugging
    if (serverProcess && serverProcess.stderr) {
      serverProcess.stderr.on('data', (data) => {
        console.error(`Server error: ${data.toString()}`);
      });
    }

    // Give the server a moment to start
    await new Promise((resolve) => setTimeout(resolve, 1000));

    // Connect the MCP client to the server
    transport = new StdioClientTransport({
      command: 'node',
      args: ['-r', 'dotenv/config', serverPath],
      env: {
        ...process.env,
        NODE_ENV: 'test',
        DOTENV_CONFIG_PATH: tempEnvFile,
      },
    });

    client = new Client(
      {
        name: 'e2e-test-client',
        version: '1.0.0',
      },
      {
        capabilities: {
          tools: {},
        },
      },
    );

    await client.connect(transport);
  });

  afterAll(async () => {
    // Clean up the client transport
    if (transport) {
      await transport.close();
    }

    // Clean up the client
    if (client) {
      await client.close();
    }

    // Clean up the server process
    if (serverProcess) {
      serverProcess.kill();
    }

    // Clean up temporary env file
    if (tempEnvFile && fs.existsSync(tempEnvFile)) {
      fs.unlinkSync(tempEnvFile);
      console.error(`Deleted temporary .env file at ${tempEnvFile}`);
    }

    // Force exit to clean up any remaining handles
    await new Promise<void>((resolve) => {
      setTimeout(() => {
        resolve();
      }, 500);
    });
  });

  describe('Organizations', () => {
    test('should list organizations', async () => {
      // Arrange
      // No specific arrangement needed for this test as we're just listing organizations

      // Act
      const result = await client.callTool({
        name: 'list_organizations',
        arguments: {},
      });

      // Assert
      expect(result).toBeDefined();

      // Access the content safely
      const content = result.content as Array<{ type: string; text: string }>;
      expect(content).toBeDefined();
      expect(content.length).toBeGreaterThan(0);

      // Parse the result content
      const resultText = content[0].text;
      const organizations: Organization[] = JSON.parse(resultText);

      // Verify the response structure
      expect(Array.isArray(organizations)).toBe(true);
      if (organizations.length > 0) {
        const firstOrg = organizations[0];
        expect(firstOrg).toHaveProperty('id');
        expect(firstOrg).toHaveProperty('name');
        expect(firstOrg).toHaveProperty('url');
      }
    });
  });

  describe('Parameterless Tools', () => {
    test('should call list_organizations without arguments', async () => {
      // Act - call the tool without providing arguments
      const result = await client.callTool({
        name: 'list_organizations',
        // No arguments provided
        arguments: {},
      });

      // Assert
      expect(result).toBeDefined();
      const content = result.content as Array<{ type: string; text: string }>;
      expect(content).toBeDefined();
      expect(content.length).toBeGreaterThan(0);

      // Verify we got a valid JSON response
      const resultText = content[0].text;
      const organizations = JSON.parse(resultText);
      expect(Array.isArray(organizations)).toBe(true);
    });

    test('should call get_me without arguments', async () => {
      // Act - call the tool without providing arguments
      const result = await client.callTool({
        name: 'get_me',
        // No arguments provided
        arguments: {},
      });

      // Assert
      expect(result).toBeDefined();
      const content = result.content as Array<{ type: string; text: string }>;
      expect(content).toBeDefined();
      expect(content.length).toBeGreaterThan(0);

      // Verify we got a valid JSON response with user info
      const resultText = content[0].text;
      const userInfo = JSON.parse(resultText);
      expect(userInfo).toHaveProperty('id');
      expect(userInfo).toHaveProperty('displayName');
    });
  });

  describe('Tools with Optional Parameters', () => {
    test('should call list_projects without arguments', async () => {
      // Act - call the tool without providing arguments
      const result = await client.callTool({
        name: 'list_projects',
        // No arguments provided
        arguments: {},
      });

      // Assert
      expect(result).toBeDefined();
      const content = result.content as Array<{ type: string; text: string }>;
      expect(content).toBeDefined();
      expect(content.length).toBeGreaterThan(0);

      // Verify we got a valid JSON response
      const resultText = content[0].text;
      const projects = JSON.parse(resultText);
      expect(Array.isArray(projects)).toBe(true);
    });

    test('should call get_project without arguments', async () => {
      // Act - call the tool without providing arguments
      const result = await client.callTool({
        name: 'get_project',
        // No arguments provided
        arguments: {},
      });

      // Assert
      expect(result).toBeDefined();
      const content = result.content as Array<{ type: string; text: string }>;
      expect(content).toBeDefined();
      expect(content.length).toBeGreaterThan(0);

      // Verify we got a valid JSON response with project info
      const resultText = content[0].text;
      const project = JSON.parse(resultText);
      expect(project).toHaveProperty('id');
      expect(project).toHaveProperty('name');
    });

    test('should call list_repositories without arguments', async () => {
      // Act - call the tool without providing arguments
      const result = await client.callTool({
        name: 'list_repositories',
        // No arguments provided
        arguments: {},
      });

      // Assert
      expect(result).toBeDefined();
      const content = result.content as Array<{ type: string; text: string }>;
      expect(content).toBeDefined();
      expect(content.length).toBeGreaterThan(0);

      // Verify we got a valid JSON response
      const resultText = content[0].text;
      const repositories = JSON.parse(resultText);
      expect(Array.isArray(repositories)).toBe(true);
    });
  });
});

```

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

```typescript
import { z } from 'zod';
import { defaultProject, defaultOrg } from '../../utils/environment';

/**
 * Schema for creating a pull request
 */
export const CreatePullRequestSchema = z.object({
  projectId: z
    .string()
    .optional()
    .describe(`The ID or name of the project (Default: ${defaultProject})`),
  organizationId: z
    .string()
    .optional()
    .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
  repositoryId: z.string().describe('The ID or name of the repository'),
  title: z.string().describe('The title of the pull request'),
  description: z
    .string()
    .optional()
    .describe('The description of the pull request (markdown is supported)'),
  sourceRefName: z
    .string()
    .describe('The source branch name (e.g., refs/heads/feature-branch)'),
  targetRefName: z
    .string()
    .describe('The target branch name (e.g., refs/heads/main)'),
  reviewers: z
    .array(z.string())
    .optional()
    .describe('List of reviewer email addresses or IDs'),
  isDraft: z
    .boolean()
    .optional()
    .describe('Whether the pull request should be created as a draft'),
  workItemRefs: z
    .array(z.number())
    .optional()
    .describe('List of work item IDs to link to the pull request'),
  tags: z
    .array(z.string().trim().min(1))
    .optional()
    .describe('List of tags to apply to the pull request'),
  additionalProperties: z
    .record(z.string(), z.any())
    .optional()
    .describe('Additional properties to set on the pull request'),
});

/**
 * Schema for listing pull requests
 */
export const ListPullRequestsSchema = z.object({
  projectId: z
    .string()
    .optional()
    .describe(`The ID or name of the project (Default: ${defaultProject})`),
  organizationId: z
    .string()
    .optional()
    .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
  repositoryId: z.string().describe('The ID or name of the repository'),
  status: z
    .enum(['all', 'active', 'completed', 'abandoned'])
    .optional()
    .describe('Filter by pull request status'),
  creatorId: z
    .string()
    .optional()
    .describe('Filter by creator ID (must be a UUID string)'),
  reviewerId: z
    .string()
    .optional()
    .describe('Filter by reviewer ID (must be a UUID string)'),
  sourceRefName: z.string().optional().describe('Filter by source branch name'),
  targetRefName: z.string().optional().describe('Filter by target branch name'),
  top: z
    .number()
    .default(10)
    .describe('Maximum number of pull requests to return (default: 10)'),
  skip: z
    .number()
    .optional()
    .describe('Number of pull requests to skip for pagination'),
  pullRequestId: z
    .number()
    .optional()
    .describe('If provided, return only the matching pull request ID'),
});

/**
 * Schema for getting pull request comments
 */
export const GetPullRequestCommentsSchema = z.object({
  projectId: z
    .string()
    .optional()
    .describe(`The ID or name of the project (Default: ${defaultProject})`),
  organizationId: z
    .string()
    .optional()
    .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
  repositoryId: z.string().describe('The ID or name of the repository'),
  pullRequestId: z.number().describe('The ID of the pull request'),
  threadId: z
    .number()
    .optional()
    .describe('The ID of the specific thread to get comments from'),
  includeDeleted: z
    .boolean()
    .optional()
    .describe('Whether to include deleted comments'),
  top: z
    .number()
    .optional()
    .describe('Maximum number of threads/comments to return'),
});

/**
 * Schema for adding a comment to a pull request
 */
export const AddPullRequestCommentSchema = z
  .object({
    projectId: z
      .string()
      .optional()
      .describe(`The ID or name of the project (Default: ${defaultProject})`),
    organizationId: z
      .string()
      .optional()
      .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
    repositoryId: z.string().describe('The ID or name of the repository'),
    pullRequestId: z.number().describe('The ID of the pull request'),
    content: z.string().describe('The content of the comment in markdown'),
    threadId: z
      .number()
      .optional()
      .describe('The ID of the thread to add the comment to'),
    parentCommentId: z
      .number()
      .optional()
      .describe(
        'ID of the parent comment when replying to an existing comment',
      ),
    filePath: z
      .string()
      .optional()
      .describe('The path of the file to comment on (for new thread on file)'),
    lineNumber: z
      .number()
      .optional()
      .describe('The line number to comment on (for new thread on file)'),
    status: z
      .enum([
        'active',
        'fixed',
        'wontFix',
        'closed',
        'pending',
        'byDesign',
        'unknown',
      ])
      .optional()
      .describe('The status to set for a new thread'),
  })
  .superRefine((data, ctx) => {
    // If we're creating a new thread (no threadId), status is required
    if (!data.threadId && !data.status) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: 'Status is required when creating a new thread',
        path: ['status'],
      });
    }
  });

/**
 * Schema for updating a pull request
 */
export const UpdatePullRequestSchema = z.object({
  projectId: z
    .string()
    .optional()
    .describe(`The ID or name of the project (Default: ${defaultProject})`),
  organizationId: z
    .string()
    .optional()
    .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
  repositoryId: z.string().describe('The ID or name of the repository'),
  pullRequestId: z.number().describe('The ID of the pull request to update'),
  title: z
    .string()
    .optional()
    .describe('The updated title of the pull request'),
  description: z
    .string()
    .optional()
    .describe('The updated description of the pull request'),
  status: z
    .enum(['active', 'abandoned', 'completed'])
    .optional()
    .describe('The updated status of the pull request'),
  isDraft: z
    .boolean()
    .optional()
    .describe(
      'Whether the pull request should be marked as a draft (true) or unmarked (false)',
    ),
  addWorkItemIds: z
    .array(z.number())
    .optional()
    .describe('List of work item IDs to link to the pull request'),
  removeWorkItemIds: z
    .array(z.number())
    .optional()
    .describe('List of work item IDs to unlink from the pull request'),
  addReviewers: z
    .array(z.string())
    .optional()
    .describe('List of reviewer email addresses or IDs to add'),
  removeReviewers: z
    .array(z.string())
    .optional()
    .describe('List of reviewer email addresses or IDs to remove'),
  addTags: z
    .array(z.string().trim().min(1))
    .optional()
    .describe('List of tags to add to the pull request'),
  removeTags: z
    .array(z.string().trim().min(1))
    .optional()
    .describe('List of tags to remove from the pull request'),
  additionalProperties: z
    .record(z.string(), z.any())
    .optional()
    .describe('Additional properties to update on the pull request'),
});

/**
 * Schema for getting pull request changes and policy evaluations
 */
export const GetPullRequestChangesSchema = z.object({
  projectId: z
    .string()
    .optional()
    .describe(`The ID or name of the project (Default: ${defaultProject})`),
  organizationId: z
    .string()
    .optional()
    .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
  repositoryId: z.string().describe('The ID or name of the repository'),
  pullRequestId: z.number().describe('The ID of the pull request'),
});

/**
 * Schema for retrieving pull request status checks and policy evaluations
 */
export const GetPullRequestChecksSchema = z.object({
  projectId: z
    .string()
    .optional()
    .describe(`The ID or name of the project (Default: ${defaultProject})`),
  organizationId: z
    .string()
    .optional()
    .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
  repositoryId: z.string().describe('The ID or name of the repository'),
  pullRequestId: z.number().describe('The ID of the pull request'),
});

export const PullRequestFileChangeSchema = z.object({
  path: z.string().describe('Path of the changed file'),
  patch: z.string().describe('Unified diff of the file'),
});

export const GetPullRequestChangesResponseSchema = z.object({
  changes: z.any(),
  evaluations: z.array(z.any()),
  files: z.array(PullRequestFileChangeSchema),
  sourceRefName: z.string().optional(),
  targetRefName: z.string().optional(),
});

```

--------------------------------------------------------------------------------
/src/features/repositories/get-repository-details/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { getRepositoryDetails } from './feature';
import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';
import {
  AzureDevOpsError,
  AzureDevOpsResourceNotFoundError,
} from '../../../shared/errors';
import { GitRepository, GitBranchStats, GitRef } from '../types';

// Unit tests should only focus on isolated logic
// No real connections, HTTP requests, or dependencies
describe('getRepositoryDetails unit', () => {
  // Mock repository data
  const mockRepository: GitRepository = {
    id: 'repo-id',
    name: 'test-repo',
    url: 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id',
    project: {
      id: 'project-id',
      name: 'test-project',
    },
    defaultBranch: 'refs/heads/main',
    size: 1024,
    remoteUrl: 'https://dev.azure.com/org/project/_git/test-repo',
    sshUrl: '[email protected]:v3/org/project/test-repo',
    webUrl: 'https://dev.azure.com/org/project/_git/test-repo',
  };

  // Mock branch stats data
  const mockBranchStats: GitBranchStats[] = [
    {
      name: 'refs/heads/main',
      aheadCount: 0,
      behindCount: 0,
      isBaseVersion: true,
      commit: {
        commitId: 'commit-id',
        author: {
          name: 'Test User',
          email: '[email protected]',
          date: new Date(),
        },
        committer: {
          name: 'Test User',
          email: '[email protected]',
          date: new Date(),
        },
        comment: 'Test commit',
      },
    },
  ];

  // Mock refs data
  const mockRefs: GitRef[] = [
    {
      name: 'refs/heads/main',
      objectId: 'commit-id',
      creator: {
        displayName: 'Test User',
        id: 'user-id',
      },
      url: 'https://dev.azure.com/org/project/_apis/git/repositories/repo-id/refs/heads/main',
    },
  ];

  test('should return basic repository information when no additional options are specified', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
      })),
    };

    // Act
    const result = await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.repository).toEqual(mockRepository);
    expect(result.statistics).toBeUndefined();
    expect(result.refs).toBeUndefined();
  });

  test('should include branch statistics when includeStatistics is true', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
        getBranches: jest.fn().mockResolvedValue(mockBranchStats),
      })),
    };

    // Act
    const result = await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
      includeStatistics: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.repository).toEqual(mockRepository);
    expect(result.statistics).toBeDefined();
    expect(result.statistics?.branches).toEqual(mockBranchStats);
    expect(result.refs).toBeUndefined();
  });

  test('should include refs when includeRefs is true', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
        getRefs: jest.fn().mockResolvedValue(mockRefs),
      })),
    };

    // Act
    const result = await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
      includeRefs: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.repository).toEqual(mockRepository);
    expect(result.statistics).toBeUndefined();
    expect(result.refs).toBeDefined();
    expect(result.refs?.value).toEqual(mockRefs);
    expect(result.refs?.count).toBe(mockRefs.length);
  });

  test('should include both statistics and refs when both options are true', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
        getBranches: jest.fn().mockResolvedValue(mockBranchStats),
        getRefs: jest.fn().mockResolvedValue(mockRefs),
      })),
    };

    // Act
    const result = await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
      includeStatistics: true,
      includeRefs: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.repository).toEqual(mockRepository);
    expect(result.statistics).toBeDefined();
    expect(result.statistics?.branches).toEqual(mockBranchStats);
    expect(result.refs).toBeDefined();
    expect(result.refs?.value).toEqual(mockRefs);
    expect(result.refs?.count).toBe(mockRefs.length);
  });

  test('should pass refFilter to getRefs when provided', async () => {
    // Arrange
    const getRefs = jest.fn().mockResolvedValue(mockRefs);
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
        getRefs,
      })),
    };

    // Act
    await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
      includeRefs: true,
      refFilter: 'heads/',
    });

    // Assert
    expect(getRefs).toHaveBeenCalledWith(
      mockRepository.id,
      'test-project',
      'heads/',
    );
  });

  test('should pass branchName to getBranches when provided', async () => {
    // Arrange
    const getBranches = jest.fn().mockResolvedValue(mockBranchStats);
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
        getBranches,
      })),
    };

    // Act
    await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
      includeStatistics: true,
      branchName: 'main',
    });

    // Assert
    expect(getBranches).toHaveBeenCalledWith(
      mockRepository.id,
      'test-project',
      {
        version: 'main',
        versionType: GitVersionType.Branch,
      },
    );
  });

  test('should propagate resource not found errors', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(null), // Simulate repository not found
      })),
    };

    // Act & Assert
    await expect(
      getRepositoryDetails(mockConnection, {
        projectId: 'test-project',
        repositoryId: 'non-existent-repo',
      }),
    ).rejects.toThrow(AzureDevOpsResourceNotFoundError);

    await expect(
      getRepositoryDetails(mockConnection, {
        projectId: 'test-project',
        repositoryId: 'non-existent-repo',
      }),
    ).rejects.toThrow(
      "Repository 'non-existent-repo' not found in project 'test-project'",
    );
  });

  test('should propagate custom errors when thrown internally', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => {
        throw new AzureDevOpsError('Custom error');
      }),
    };

    // Act & Assert
    await expect(
      getRepositoryDetails(mockConnection, {
        projectId: 'test-project',
        repositoryId: 'test-repo',
      }),
    ).rejects.toThrow(AzureDevOpsError);

    await expect(
      getRepositoryDetails(mockConnection, {
        projectId: 'test-project',
        repositoryId: 'test-repo',
      }),
    ).rejects.toThrow('Custom error');
  });

  test('should wrap unexpected errors in a friendly error message', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => {
        throw new Error('Unexpected error');
      }),
    };

    // Act & Assert
    await expect(
      getRepositoryDetails(mockConnection, {
        projectId: 'test-project',
        repositoryId: 'test-repo',
      }),
    ).rejects.toThrow('Failed to get repository details: Unexpected error');
  });

  test('should handle null refs gracefully', async () => {
    // Arrange
    const mockConnection: any = {
      getGitApi: jest.fn().mockImplementation(() => ({
        getRepository: jest.fn().mockResolvedValue(mockRepository),
        getRefs: jest.fn().mockResolvedValue(null), // Simulate null refs
      })),
    };

    // Act
    const result = await getRepositoryDetails(mockConnection, {
      projectId: 'test-project',
      repositoryId: 'test-repo',
      includeRefs: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.repository).toEqual(mockRepository);
    expect(result.refs).toBeDefined();
    expect(result.refs?.value).toEqual([]);
    expect(result.refs?.count).toBe(0);
  });
});

```

--------------------------------------------------------------------------------
/memory/tasks_memory_2025-05-26T16-18-03.json:
--------------------------------------------------------------------------------

```json
{
  "tasks": [
    {
      "id": "1c881b0f-7fd6-4184-89f5-1676a56e3719",
      "name": "Fix shared type definitions with explicit any warnings",
      "description": "Replace explicit 'any' types in shared type definitions with proper TypeScript types to resolve ESLint warnings. This includes RequestHandler return type and ToolDefinition inputSchema type.",
      "notes": "These are core type definitions used throughout the project, so changes must maintain backward compatibility. The CallToolResult type is the standard MCP SDK return type for tool responses.",
      "status": "completed",
      "dependencies": [],
      "createdAt": "2025-05-26T15:23:09.065Z",
      "updatedAt": "2025-05-26T15:33:17.167Z",
      "relatedFiles": [
        {
          "path": "src/shared/types/request-handler.ts",
          "type": "TO_MODIFY",
          "description": "Contains RequestHandler interface with 'any' return type",
          "lineStart": 14,
          "lineEnd": 16
        },
        {
          "path": "src/shared/types/tool-definition.ts",
          "type": "TO_MODIFY",
          "description": "Contains ToolDefinition interface with 'any' inputSchema type",
          "lineStart": 4,
          "lineEnd": 8
        }
      ],
      "implementationGuide": "1. Update src/shared/types/request-handler.ts line 15: Change 'any' to 'Promise<CallToolResult>' where CallToolResult is imported from '@modelcontextprotocol/sdk/types.js'\n2. Update src/shared/types/tool-definition.ts line 7: Change 'any' to 'JSONSchema7' where JSONSchema7 is imported from 'json-schema'\n3. Add necessary imports at the top of each file\n4. Ensure all existing functionality remains unchanged",
      "verificationCriteria": "1. ESLint warnings for these files should be resolved\n2. TypeScript compilation should succeed\n3. All existing tests should continue to pass\n4. Import statements should be properly added",
      "analysisResult": "Fix lint issues and get unit tests passing in the MCP Azure DevOps server project. The solution addresses 18 TypeScript 'any' type warnings by replacing them with proper types from Azure DevOps Node API and MCP SDK, and resolves 1 failing unit test by handling empty test files. All changes maintain backward compatibility and follow existing project patterns.",
      "summary": "Successfully replaced explicit 'any' types in shared type definitions with proper TypeScript types. Updated RequestHandler return type to use CallToolResult union type for backward compatibility, and ToolDefinition inputSchema to use JsonSchema7Type from zod-to-json-schema. ESLint warnings for these files are resolved, TypeScript compilation succeeds for the core types, and all existing functionality remains unchanged with proper imports added.",
      "completedAt": "2025-05-26T15:33:17.165Z"
    },
    {
      "id": "17aa94fe-24d4-4a8b-a127-ef27e121de38",
      "name": "Fix Azure DevOps client type warnings",
      "description": "Replace 'any' types in Azure DevOps client files with proper types from the azure-devops-node-api library to resolve 7 ESLint warnings in src/clients/azure-devops.ts.",
      "notes": "The azure-devops-node-api library provides comprehensive TypeScript interfaces. Prefer using existing library types over creating custom ones.",
      "status": "completed",
      "dependencies": [
        {
          "taskId": "1c881b0f-7fd6-4184-89f5-1676a56e3719"
        }
      ],
      "createdAt": "2025-05-26T15:23:09.065Z",
      "updatedAt": "2025-05-26T15:39:30.003Z",
      "relatedFiles": [
        {
          "path": "src/clients/azure-devops.ts",
          "type": "TO_MODIFY",
          "description": "Contains 7 'any' type warnings that need proper typing",
          "lineStart": 1,
          "lineEnd": 500
        }
      ],
      "implementationGuide": "1. Examine each 'any' usage in src/clients/azure-devops.ts at lines 78, 158, 244, 305, 345, 453, 484\n2. Replace with appropriate types from azure-devops-node-api interfaces\n3. Common patterns: Use TeamProject, GitRepository, WorkItem, BuildDefinition types\n4. For API responses, use the specific interface types provided by the library\n5. If no specific type exists, create a minimal interface with required properties",
      "verificationCriteria": "1. All 7 ESLint warnings in src/clients/azure-devops.ts should be resolved\n2. TypeScript compilation should succeed\n3. Existing functionality should remain unchanged\n4. Types should be imported from azure-devops-node-api where available",
      "analysisResult": "Fix lint issues and get unit tests passing in the MCP Azure DevOps server project. The solution addresses 18 TypeScript 'any' type warnings by replacing them with proper types from Azure DevOps Node API and MCP SDK, and resolves 1 failing unit test by handling empty test files. All changes maintain backward compatibility and follow existing project patterns.",
      "summary": "Successfully fixed all 7 ESLint warnings in src/clients/azure-devops.ts by replacing 'any' types with proper TypeScript interfaces. Created AzureDevOpsApiErrorResponse interface for Azure DevOps API error responses and replaced Record<string, any> with Record<string, string> for payload objects. All ESLint warnings are now resolved while maintaining existing functionality and backward compatibility.",
      "completedAt": "2025-05-26T15:39:30.002Z"
    },
    {
      "id": "d971e510-94cc-4f12-a1e8-a0ac35d57b7f",
      "name": "Fix feature-specific type warnings",
      "description": "Replace 'any' types in feature modules with proper Azure DevOps API types to resolve remaining ESLint warnings in projects, pull-requests, and repositories features.",
      "notes": "Each feature module should use the most specific Azure DevOps API type available. Check existing working features for type usage patterns.",
      "status": "completed",
      "dependencies": [
        {
          "taskId": "1c881b0f-7fd6-4184-89f5-1676a56e3719"
        }
      ],
      "createdAt": "2025-05-26T15:23:09.065Z",
      "updatedAt": "2025-05-26T15:51:24.788Z",
      "relatedFiles": [
        {
          "path": "src/features/projects/get-project-details/feature.ts",
          "type": "TO_MODIFY",
          "description": "Contains 'any' type warning at line 198"
        },
        {
          "path": "src/features/pull-requests/types.ts",
          "type": "TO_MODIFY",
          "description": "Contains 'any' type warnings at lines 20, 83"
        },
        {
          "path": "src/features/pull-requests/update-pull-request/feature.ts",
          "type": "TO_MODIFY",
          "description": "Contains 'any' type warnings at lines 33, 144, 213, 254"
        },
        {
          "path": "src/features/repositories/get-all-repositories-tree/feature.ts",
          "type": "TO_MODIFY",
          "description": "Contains 'any' type warning at line 231"
        },
        {
          "path": "src/shared/auth/client-factory.ts",
          "type": "TO_MODIFY",
          "description": "Contains 'any' type warning at line 282"
        }
      ],
      "implementationGuide": "1. Fix src/features/projects/get-project-details/feature.ts line 198: Use TeamProject or TeamProjectReference type\n2. Fix src/features/pull-requests/types.ts lines 20, 83: Use GitPullRequest related interfaces\n3. Fix src/features/pull-requests/update-pull-request/feature.ts lines 33, 144, 213, 254: Use GitPullRequest and JsonPatchOperation types\n4. Fix src/features/repositories/get-all-repositories-tree/feature.ts line 231: Use GitTreeRef or GitItem type\n5. Fix src/shared/auth/client-factory.ts line 282: Use proper authentication credential type\n6. Import types from azure-devops-node-api/interfaces/",
      "verificationCriteria": "1. All remaining ESLint 'any' type warnings should be resolved\n2. TypeScript compilation should succeed\n3. All existing tests should continue to pass\n4. Types should be consistent with Azure DevOps API documentation",
      "analysisResult": "Fix lint issues and get unit tests passing in the MCP Azure DevOps server project. The solution addresses 18 TypeScript 'any' type warnings by replacing them with proper types from Azure DevOps Node API and MCP SDK, and resolves 1 failing unit test by handling empty test files. All changes maintain backward compatibility and follow existing project patterns.",
      "summary": "Successfully fixed all feature-specific type warnings by replacing 'any' types with proper Azure DevOps API types. Fixed src/features/projects/get-project-details/feature.ts by using WorkItemTypeField interface, src/features/pull-requests/types.ts by replacing 'any' with specific union types, src/features/pull-requests/update-pull-request/feature.ts by using WebApi, AuthenticationMethod, and WorkItemRelation types, src/features/repositories/get-all-repositories-tree/feature.ts by using IGitApi type, and src/shared/auth/client-factory.ts by using IProfileApi type. All ESLint 'any' type warnings in the specified files have been resolved while maintaining type safety and consistency with Azure DevOps API documentation.",
      "completedAt": "2025-05-26T15:51:24.787Z"
    }
  ]
}
```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { ICoreApi } from 'azure-devops-node-api/CoreApi';
import { IGitApi } from 'azure-devops-node-api/GitApi';
import { IWorkItemTrackingApi } from 'azure-devops-node-api/WorkItemTrackingApi';
import { IBuildApi } from 'azure-devops-node-api/BuildApi';
import { ITestApi } from 'azure-devops-node-api/TestApi';
import { IReleaseApi } from 'azure-devops-node-api/ReleaseApi';
import { ITaskAgentApi } from 'azure-devops-node-api/TaskAgentApi';
import { ITaskApi } from 'azure-devops-node-api/TaskApi';
import { IProfileApi } from 'azure-devops-node-api/ProfileApi';
import { AzureDevOpsError, AzureDevOpsAuthenticationError } from '../errors';
import { AuthConfig, createAuthClient } from './auth-factory';

/**
 * Azure DevOps Client
 *
 * Provides access to Azure DevOps APIs using the configured authentication method
 */
export class AzureDevOpsClient {
  private config: AuthConfig;
  private clientPromise: Promise<WebApi> | null = null;

  /**
   * Creates a new Azure DevOps client
   *
   * @param config Authentication configuration
   */
  constructor(config: AuthConfig) {
    this.config = config;
  }

  /**
   * Get the authenticated Azure DevOps client
   *
   * @returns The authenticated WebApi client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  private async getClient(): Promise<WebApi> {
    if (!this.clientPromise) {
      this.clientPromise = (async () => {
        try {
          return await createAuthClient(this.config);
        } catch (error) {
          // If it's already an AzureDevOpsError, rethrow it
          if (error instanceof AzureDevOpsError) {
            throw error;
          }
          // Otherwise, wrap it in an AzureDevOpsAuthenticationError
          throw new AzureDevOpsAuthenticationError(
            error instanceof Error
              ? `Authentication failed: ${error.message}`
              : 'Authentication failed: Unknown error',
          );
        }
      })();
    }
    return this.clientPromise;
  }

  /**
   * Get the underlying WebApi client
   *
   * @returns The authenticated WebApi client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getWebApiClient(): Promise<WebApi> {
    return this.getClient();
  }

  /**
   * Check if the client is authenticated
   *
   * @returns True if the client is authenticated
   */
  public async isAuthenticated(): Promise<boolean> {
    try {
      const client = await this.getClient();
      return !!client;
    } catch {
      // Any error means we're not authenticated
      return false;
    }
  }

  /**
   * Get the Core API
   *
   * @returns The Core API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getCoreApi(): Promise<ICoreApi> {
    try {
      const client = await this.getClient();
      return await client.getCoreApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Core API: ${error.message}`
          : 'Failed to get Core API: Unknown error',
      );
    }
  }

  /**
   * Get the Git API
   *
   * @returns The Git API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getGitApi(): Promise<IGitApi> {
    try {
      const client = await this.getClient();
      return await client.getGitApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Git API: ${error.message}`
          : 'Failed to get Git API: Unknown error',
      );
    }
  }

  /**
   * Get the Work Item Tracking API
   *
   * @returns The Work Item Tracking API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getWorkItemTrackingApi(): Promise<IWorkItemTrackingApi> {
    try {
      const client = await this.getClient();
      return await client.getWorkItemTrackingApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Work Item Tracking API: ${error.message}`
          : 'Failed to get Work Item Tracking API: Unknown error',
      );
    }
  }

  /**
   * Get the Build API
   *
   * @returns The Build API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getBuildApi(): Promise<IBuildApi> {
    try {
      const client = await this.getClient();
      return await client.getBuildApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Build API: ${error.message}`
          : 'Failed to get Build API: Unknown error',
      );
    }
  }

  /**
   * Get the Test API
   *
   * @returns The Test API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getTestApi(): Promise<ITestApi> {
    try {
      const client = await this.getClient();
      return await client.getTestApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Test API: ${error.message}`
          : 'Failed to get Test API: Unknown error',
      );
    }
  }

  /**
   * Get the Release API
   *
   * @returns The Release API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getReleaseApi(): Promise<IReleaseApi> {
    try {
      const client = await this.getClient();
      return await client.getReleaseApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Release API: ${error.message}`
          : 'Failed to get Release API: Unknown error',
      );
    }
  }

  /**
   * Get the Task Agent API
   *
   * @returns The Task Agent API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getTaskAgentApi(): Promise<ITaskAgentApi> {
    try {
      const client = await this.getClient();
      return await client.getTaskAgentApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Task Agent API: ${error.message}`
          : 'Failed to get Task Agent API: Unknown error',
      );
    }
  }

  /**
   * Get the Task API
   *
   * @returns The Task API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getTaskApi(): Promise<ITaskApi> {
    try {
      const client = await this.getClient();
      return await client.getTaskApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Task API: ${error.message}`
          : 'Failed to get Task API: Unknown error',
      );
    }
  }

  /**
   * Get the Profile API
   *
   * @returns The Profile API client
   * @throws {AzureDevOpsAuthenticationError} If authentication fails
   */
  public async getProfileApi(): Promise<IProfileApi> {
    try {
      const client = await this.getClient();
      return await client.getProfileApi();
    } catch (error) {
      // If it's already an AzureDevOpsError, rethrow it
      if (error instanceof AzureDevOpsError) {
        throw error;
      }
      // Otherwise, wrap it in an AzureDevOpsAuthenticationError
      throw new AzureDevOpsAuthenticationError(
        error instanceof Error
          ? `Failed to get Profile API: ${error.message}`
          : 'Failed to get Profile API: Unknown error',
      );
    }
  }
}

```

--------------------------------------------------------------------------------
/src/features/pull-requests/get-pull-request-comments/feature.spec.int.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import { getPullRequestComments } from './feature';
import { listPullRequests } from '../list-pull-requests/feature';
import { addPullRequestComment } from '../add-pull-request-comment/feature';
import {
  getTestConnection,
  shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';

describe('getPullRequestComments integration', () => {
  let connection: WebApi | null = null;
  let projectName: string;
  let repositoryName: string;
  let pullRequestId: number;
  let testThreadId: number;

  // Generate unique identifiers using timestamp for comment content
  const timestamp = Date.now();
  const randomSuffix = Math.floor(Math.random() * 1000);

  beforeAll(async () => {
    // Get a real connection using environment variables
    connection = await getTestConnection();

    // Set up project and repository names from environment
    projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
    repositoryName = process.env.AZURE_DEVOPS_DEFAULT_REPOSITORY || '';

    // Skip setup if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      return;
    }

    try {
      // Find an active pull request to use for testing
      const pullRequests = await listPullRequests(
        connection,
        projectName,
        repositoryName,
        {
          projectId: projectName,
          repositoryId: repositoryName,
          status: 'active',
          top: 1,
        },
      );

      if (!pullRequests || pullRequests.value.length === 0) {
        throw new Error('No active pull requests found for testing');
      }

      pullRequestId = pullRequests.value[0].pullRequestId!;
      console.log(`Using existing pull request #${pullRequestId} for testing`);

      // Create a test comment thread that we can use for specific thread tests
      const result = await addPullRequestComment(
        connection,
        projectName,
        repositoryName,
        pullRequestId,
        {
          projectId: projectName,
          repositoryId: repositoryName,
          pullRequestId,
          content: `Test comment thread ${timestamp}-${randomSuffix}`,
          status: 'active',
        },
      );

      testThreadId = result.thread!.id!;
      console.log(`Created test comment thread #${testThreadId} for testing`);
    } catch (error) {
      console.error('Error in test setup:', error);
      throw error;
    }
  });

  test('should get all comment threads from pull request with file path and line number', async () => {
    // Skip if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      console.log('Skipping test due to missing connection');
      return;
    }

    // Skip if repository name is not defined
    if (!repositoryName) {
      console.log('Skipping test due to missing repository name');
      return;
    }

    const threads = await getPullRequestComments(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
      },
    );

    // Verify threads were returned
    expect(threads).toBeDefined();
    expect(Array.isArray(threads)).toBe(true);
    expect(threads.length).toBeGreaterThan(0);

    // Verify thread structure
    const firstThread = threads[0];
    expect(firstThread.id).toBeDefined();
    expect(firstThread.comments).toBeDefined();
    expect(Array.isArray(firstThread.comments)).toBe(true);
    expect(firstThread.comments!.length).toBeGreaterThan(0);

    // Verify comment structure including new fields
    const firstComment = firstThread.comments![0];
    expect(firstComment.content).toBeDefined();
    expect(firstComment.id).toBeDefined();
    expect(firstComment.publishedDate).toBeDefined();
    expect(firstComment.author).toBeDefined();

    // Verify new fields are present (may be undefined/null for general comments)
    expect(firstComment).toHaveProperty('filePath');
    expect(firstComment).toHaveProperty('rightFileStart');
    expect(firstComment).toHaveProperty('rightFileEnd');
    expect(firstComment).toHaveProperty('leftFileStart');
    expect(firstComment).toHaveProperty('leftFileEnd');
  }, 30000);

  test('should get a specific comment thread by ID with file path and line number', async () => {
    // Skip if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      console.log('Skipping test due to missing connection');
      return;
    }

    // Skip if repository name is not defined
    if (!repositoryName) {
      console.log('Skipping test due to missing repository name');
      return;
    }

    const threads = await getPullRequestComments(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        threadId: testThreadId,
      },
    );

    // Verify only one thread was returned
    expect(threads).toBeDefined();
    expect(Array.isArray(threads)).toBe(true);
    expect(threads.length).toBe(1);

    // Verify it's the correct thread
    const thread = threads[0];
    expect(thread.id).toBe(testThreadId);
    expect(thread.comments).toBeDefined();
    expect(Array.isArray(thread.comments)).toBe(true);
    expect(thread.comments!.length).toBeGreaterThan(0);

    // Verify the comment content matches what we created
    const comment = thread.comments![0];
    expect(comment.content).toBe(
      `Test comment thread ${timestamp}-${randomSuffix}`,
    );

    // Verify new fields are present (may be undefined/null for general comments)
    expect(comment).toHaveProperty('filePath');
    expect(comment).toHaveProperty('rightFileStart');
    expect(comment).toHaveProperty('rightFileEnd');
    expect(comment).toHaveProperty('leftFileStart');
    expect(comment).toHaveProperty('leftFileEnd');
  }, 30000);

  test('should handle pagination with top parameter', async () => {
    // Skip if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      console.log('Skipping test due to missing connection');
      return;
    }

    // Skip if repository name is not defined
    if (!repositoryName) {
      console.log('Skipping test due to missing repository name');
      return;
    }

    // Get all threads first to compare
    const allThreads = await getPullRequestComments(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
      },
    );

    // Then get with pagination
    const paginatedThreads = await getPullRequestComments(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        top: 1,
      },
    );

    // Verify pagination
    expect(paginatedThreads).toBeDefined();
    expect(Array.isArray(paginatedThreads)).toBe(true);
    expect(paginatedThreads.length).toBe(1);
    expect(paginatedThreads.length).toBeLessThanOrEqual(allThreads.length);

    // Verify the thread structure is the same
    const thread = paginatedThreads[0];
    expect(thread.id).toBeDefined();
    expect(thread.comments).toBeDefined();
    expect(Array.isArray(thread.comments)).toBe(true);
    expect(thread.comments!.length).toBeGreaterThan(0);

    // Verify new fields are present in paginated results
    const comment = thread.comments![0];
    expect(comment).toHaveProperty('filePath');
    expect(comment).toHaveProperty('rightFileStart');
    expect(comment).toHaveProperty('rightFileEnd');
    expect(comment).toHaveProperty('leftFileStart');
    expect(comment).toHaveProperty('leftFileEnd');
  }, 30000);

  test('should handle includeDeleted parameter', async () => {
    // Skip if integration tests should be skipped
    if (shouldSkipIntegrationTest() || !connection) {
      console.log('Skipping test due to missing connection');
      return;
    }

    // Skip if repository name is not defined
    if (!repositoryName) {
      console.log('Skipping test due to missing repository name');
      return;
    }

    const threads = await getPullRequestComments(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        includeDeleted: true,
      },
    );

    // We can only verify the call succeeds, as we can't guarantee deleted comments exist
    expect(threads).toBeDefined();
    expect(Array.isArray(threads)).toBe(true);

    // If there are any threads, verify they have the new fields
    if (threads.length > 0) {
      const thread = threads[0];
      if (thread.comments && thread.comments.length > 0) {
        const comment = thread.comments[0];
        expect(comment).toHaveProperty('filePath');
        expect(comment).toHaveProperty('rightFileStart');
        expect(comment).toHaveProperty('rightFileEnd');
        expect(comment).toHaveProperty('leftFileStart');
        expect(comment).toHaveProperty('leftFileEnd');
      }
    }
  }, 30000); // 30 second timeout for integration test
});

```

--------------------------------------------------------------------------------
/src/features/pull-requests/update-pull-request/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { updatePullRequest } from './feature';
import { AzureDevOpsClient } from '../../../shared/auth/client-factory';
import { AzureDevOpsError } from '../../../shared/errors';

// Mock the AzureDevOpsClient
jest.mock('../../../shared/auth/client-factory');

describe('updatePullRequest', () => {
  const mockGetPullRequestById = jest.fn();
  const mockUpdatePullRequest = jest.fn();
  const mockGetPullRequestLabels = jest.fn();
  const mockCreatePullRequestLabel = jest.fn();
  const mockDeletePullRequestLabels = jest.fn();
  const mockUpdateWorkItem = jest.fn();
  const mockGetWorkItem = jest.fn();

  // Mock Git API
  const mockGitApi = {
    getPullRequestById: mockGetPullRequestById,
    updatePullRequest: mockUpdatePullRequest,
    getPullRequestLabels: mockGetPullRequestLabels,
    createPullRequestLabel: mockCreatePullRequestLabel,
    deletePullRequestLabels: mockDeletePullRequestLabels,
  };

  // Mock Work Item Tracking API
  const mockWorkItemTrackingApi = {
    updateWorkItem: mockUpdateWorkItem,
    getWorkItem: mockGetWorkItem,
  };

  // Mock connection
  const mockConnection = {
    getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    getWorkItemTrackingApi: jest
      .fn()
      .mockResolvedValue(mockWorkItemTrackingApi),
  };

  const mockAzureDevopsClient = {
    getWebApiClient: jest.fn().mockResolvedValue(mockConnection),
    // ...other properties if needed
  };

  beforeEach(() => {
    jest.clearAllMocks();
    (AzureDevOpsClient as unknown as jest.Mock).mockImplementation(
      () => mockAzureDevopsClient,
    );
    mockGetPullRequestLabels.mockResolvedValue([]);
  });

  it('should throw error when pull request does not exist', async () => {
    mockGetPullRequestById.mockResolvedValueOnce(null);

    await expect(
      updatePullRequest({
        projectId: 'project-1',
        repositoryId: 'repo1',
        pullRequestId: 123,
      }),
    ).rejects.toThrow(AzureDevOpsError);
  });

  it('should update the pull request title and description', async () => {
    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
    });

    mockUpdatePullRequest.mockResolvedValueOnce({
      title: 'Updated Title',
      description: 'Updated Description',
    });

    const result = await updatePullRequest({
      projectId: 'project-1',
      repositoryId: 'repo1',
      pullRequestId: 123,
      title: 'Updated Title',
      description: 'Updated Description',
    });

    expect(mockUpdatePullRequest).toHaveBeenCalledWith(
      {
        title: 'Updated Title',
        description: 'Updated Description',
      },
      'repo1',
      123,
      'project-1',
    );

    expect(result).toEqual({
      title: 'Updated Title',
      description: 'Updated Description',
    });
  });

  it('should update the pull request status when status is provided', async () => {
    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
    });

    mockUpdatePullRequest.mockResolvedValueOnce({
      status: 2, // Abandoned
    });

    const result = await updatePullRequest({
      projectId: 'project-1',
      repositoryId: 'repo1',
      pullRequestId: 123,
      status: 'abandoned',
    });

    expect(mockUpdatePullRequest).toHaveBeenCalledWith(
      {
        status: 2, // Abandoned value
      },
      'repo1',
      123,
      'project-1',
    );

    expect(result).toEqual({
      status: 2, // Abandoned
    });
  });

  it('should throw error for invalid status', async () => {
    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
    });

    await expect(
      updatePullRequest({
        projectId: 'project-1',
        repositoryId: 'repo1',
        pullRequestId: 123,
        status: 'invalid-status' as any,
      }),
    ).rejects.toThrow(AzureDevOpsError);
  });

  it('should update the pull request draft status', async () => {
    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
    });

    mockUpdatePullRequest.mockResolvedValueOnce({
      isDraft: true,
    });

    const result = await updatePullRequest({
      projectId: 'project-1',
      repositoryId: 'repo1',
      pullRequestId: 123,
      isDraft: true,
    });

    expect(mockUpdatePullRequest).toHaveBeenCalledWith(
      {
        isDraft: true,
      },
      'repo1',
      123,
      'project-1',
    );

    expect(result).toEqual({
      isDraft: true,
    });
  });

  it('should include additionalProperties in the update', async () => {
    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
    });

    mockUpdatePullRequest.mockResolvedValueOnce({
      title: 'Title',
      customProperty: 'custom value',
    });

    const result = await updatePullRequest({
      projectId: 'project-1',
      repositoryId: 'repo1',
      pullRequestId: 123,
      additionalProperties: {
        customProperty: 'custom value',
      },
    });

    expect(mockUpdatePullRequest).toHaveBeenCalledWith(
      {
        customProperty: 'custom value',
      },
      'repo1',
      123,
      'project-1',
    );

    expect(result).toEqual({
      title: 'Title',
      customProperty: 'custom value',
    });
  });

  it('should add new tags to the pull request', async () => {
    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
      pullRequestId: 123,
    });

    mockUpdatePullRequest.mockResolvedValueOnce({
      pullRequestId: 123,
    });

    mockGetPullRequestLabels.mockResolvedValueOnce([{ name: 'existing' }]);

    mockCreatePullRequestLabel.mockImplementation(
      async (label: { name: string }) => ({
        name: label.name,
      }),
    );

    const result = await updatePullRequest({
      projectId: 'project-1',
      repositoryId: 'repo1',
      pullRequestId: 123,
      addTags: ['New Tag', 'new tag', 'Another'],
    });

    expect(mockGetPullRequestLabels).toHaveBeenCalledWith(
      'repo1',
      123,
      'project-1',
    );
    expect(mockCreatePullRequestLabel).toHaveBeenCalledTimes(2);
    expect(mockCreatePullRequestLabel).toHaveBeenCalledWith(
      { name: 'New Tag' },
      'repo1',
      123,
      'project-1',
    );
    expect(mockCreatePullRequestLabel).toHaveBeenCalledWith(
      { name: 'Another' },
      'repo1',
      123,
      'project-1',
    );
    expect(result.labels).toEqual([
      { name: 'existing' },
      { name: 'New Tag' },
      { name: 'Another' },
    ]);
  });

  it('should remove tags and ignore missing ones', async () => {
    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
      pullRequestId: 123,
    });

    mockUpdatePullRequest.mockResolvedValueOnce({
      pullRequestId: 123,
    });

    mockGetPullRequestLabels.mockResolvedValueOnce([
      { name: 'keep' },
      { name: 'remove' },
    ]);

    mockDeletePullRequestLabels
      .mockResolvedValueOnce(undefined)
      .mockRejectedValueOnce({ statusCode: 404 });

    const result = await updatePullRequest({
      projectId: 'project-1',
      repositoryId: 'repo1',
      pullRequestId: 123,
      removeTags: ['remove', 'missing'],
    });

    expect(mockDeletePullRequestLabels).toHaveBeenCalledTimes(2);
    expect(mockDeletePullRequestLabels).toHaveBeenCalledWith(
      'repo1',
      123,
      'remove',
      'project-1',
    );
    expect(mockDeletePullRequestLabels).toHaveBeenCalledWith(
      'repo1',
      123,
      'missing',
      'project-1',
    );
    expect(result.labels).toEqual([{ name: 'keep' }]);
  });

  it('should handle work item links', async () => {
    // Define the artifactId that will be used
    const artifactId = 'vstfs:///Git/PullRequestId/project-1/repo1/123';

    mockGetPullRequestById.mockResolvedValueOnce({
      repository: { id: 'repo1' },
      artifactId: artifactId, // Add the artifactId to the mock response
    });

    mockUpdatePullRequest.mockResolvedValueOnce({
      pullRequestId: 123,
      repository: { id: 'repo1' },
      artifactId: artifactId,
    });

    // Mocks for work items to remove
    mockGetWorkItem.mockResolvedValueOnce({
      relations: [
        {
          rel: 'ArtifactLink',
          url: artifactId, // Use the same artifactId here
          attributes: {
            name: 'Pull Request',
          },
        },
      ],
    });

    mockGetWorkItem.mockResolvedValueOnce({
      relations: [
        {
          rel: 'ArtifactLink',
          url: artifactId, // Use the same artifactId here
          attributes: {
            name: 'Pull Request',
          },
        },
      ],
    });

    await updatePullRequest({
      projectId: 'project-1',
      repositoryId: 'repo1',
      pullRequestId: 123,
      addWorkItemIds: [456, 789],
      removeWorkItemIds: [101, 202],
    });

    // Check that updateWorkItem was called for adding work items
    expect(mockUpdateWorkItem).toHaveBeenCalledTimes(4); // 2 for add, 2 for remove
    expect(mockUpdateWorkItem).toHaveBeenCalledWith(
      null,
      [
        {
          op: 'add',
          path: '/relations/-',
          value: {
            rel: 'ArtifactLink',
            url: 'vstfs:///Git/PullRequestId/project-1/repo1/123',
            attributes: {
              name: 'Pull Request',
            },
          },
        },
      ],
      456,
    );

    // Check for removing work items
    expect(mockUpdateWorkItem).toHaveBeenCalledWith(
      null,
      [
        {
          op: 'remove',
          path: '/relations/0',
        },
      ],
      101,
    );
  });

  it('should wrap unexpected errors in a friendly error message', async () => {
    mockGetPullRequestById.mockRejectedValueOnce(new Error('Unexpected'));

    await expect(
      updatePullRequest({
        projectId: 'project-1',
        repositoryId: 'repo1',
        pullRequestId: 123,
      }),
    ).rejects.toThrow(AzureDevOpsError);
  });
});

```
Page 4/7FirstPrevNextLast