#
tokens: 49591/50000 37/335 files (page 3/7)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 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

--------------------------------------------------------------------------------
/src/features/pipelines/pipeline-timeline/feature.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import {
  TimelineRecord,
  TimelineRecordState,
  TaskResult,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
import {
  AzureDevOpsAuthenticationError,
  AzureDevOpsError,
  AzureDevOpsResourceNotFoundError,
} from '../../../shared/errors';
import { defaultProject } from '../../../utils/environment';
import { GetPipelineTimelineOptions, PipelineTimeline } from '../types';

const API_VERSION = '7.1';

export async function getPipelineTimeline(
  connection: WebApi,
  options: GetPipelineTimelineOptions,
): Promise<PipelineTimeline> {
  try {
    const buildApi = await connection.getBuildApi();
    const projectId = options.projectId ?? defaultProject;
    const { runId, timelineId, state, result } = options;

    const route = `${encodeURIComponent(projectId)}/_apis/build/builds/${runId}/timeline`;
    const baseUrl = connection.serverUrl.replace(/\/+$/, '');
    const url = new URL(`${route}`, `${baseUrl}/`);
    url.searchParams.set('api-version', API_VERSION);
    if (timelineId) {
      url.searchParams.set('timelineId', timelineId);
    }

    const requestOptions = buildApi.createRequestOptions(
      'application/json',
      API_VERSION,
    );

    const response = await buildApi.rest.get<PipelineTimeline | null>(
      url.toString(),
      requestOptions,
    );

    if (response.statusCode === 404 || !response.result) {
      throw new AzureDevOpsResourceNotFoundError(
        `Timeline not found for run ${runId} in project ${projectId}`,
      );
    }

    const timeline = response.result as PipelineTimeline & {
      records?: TimelineRecord[];
    };
    const stateFilters = normalizeFilter(state);
    const resultFilters = normalizeFilter(result);

    if (Array.isArray(timeline.records) && (stateFilters || resultFilters)) {
      const filteredRecords = timeline.records.filter((record) => {
        const recordState = stateToString(record.state);
        const recordResult = resultToString(record.result);

        const stateMatch =
          !stateFilters || (recordState && stateFilters.has(recordState));
        const resultMatch =
          !resultFilters || (recordResult && resultFilters.has(recordResult));

        return stateMatch && resultMatch;
      });

      return {
        ...timeline,
        records: filteredRecords,
      } as PipelineTimeline;
    }

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

    if (error instanceof Error) {
      const message = error.message.toLowerCase();
      if (
        message.includes('authentication') ||
        message.includes('unauthorized') ||
        message.includes('401')
      ) {
        throw new AzureDevOpsAuthenticationError(
          `Failed to authenticate: ${error.message}`,
        );
      }

      if (
        message.includes('not found') ||
        message.includes('does not exist') ||
        message.includes('404')
      ) {
        throw new AzureDevOpsResourceNotFoundError(
          `Pipeline timeline or project not found: ${error.message}`,
        );
      }
    }

    throw new AzureDevOpsError(
      `Failed to retrieve pipeline timeline: ${
        error instanceof Error ? error.message : String(error)
      }`,
    );
  }
}

function normalizeFilter(value?: string | string[]): Set<string> | undefined {
  if (!value) {
    return undefined;
  }

  const values = Array.isArray(value) ? value : [value];
  const normalized = values
    .map((item) => (typeof item === 'string' ? item.trim().toLowerCase() : ''))
    .filter((item) => item.length > 0);

  return normalized.length > 0 ? new Set(normalized) : undefined;
}

function stateToString(
  state?: TimelineRecordState | string,
): string | undefined {
  if (typeof state === 'number') {
    const stateName = TimelineRecordState[state];
    return typeof stateName === 'string' ? stateName.toLowerCase() : undefined;
  }

  if (typeof state === 'string' && state.length > 0) {
    return state.toLowerCase();
  }

  return undefined;
}

function resultToString(result?: TaskResult | string): string | undefined {
  if (typeof result === 'number') {
    const resultName = TaskResult[result];
    return typeof resultName === 'string'
      ? resultName.toLowerCase()
      : undefined;
  }

  if (typeof result === 'string' && result.length > 0) {
    return result.toLowerCase();
  }

  return undefined;
}

```

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

```typescript
export { getWikis, GetWikisSchema } from './get-wikis';
export { getWikiPage, GetWikiPageSchema } from './get-wiki-page';
export { createWiki, CreateWikiSchema, WikiType } from './create-wiki';
export { updateWikiPage, UpdateWikiPageSchema } from './update-wiki-page';
export { listWikiPages, ListWikiPagesSchema } from './list-wiki-pages';
export { createWikiPage, CreateWikiPageSchema } from './create-wiki-page';

// 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 {
  RequestIdentifier,
  RequestHandler,
} from '../../shared/types/request-handler';
import { defaultProject, defaultOrg } from '../../utils/environment';
import {
  GetWikisSchema,
  GetWikiPageSchema,
  CreateWikiSchema,
  UpdateWikiPageSchema,
  ListWikiPagesSchema,
  CreateWikiPageSchema,
  getWikis,
  getWikiPage,
  createWiki,
  updateWikiPage,
  listWikiPages,
  createWikiPage,
} from './';

/**
 * Checks if the request is for the wikis feature
 */
export const isWikisRequest: RequestIdentifier = (
  request: CallToolRequest,
): boolean => {
  const toolName = request.params.name;
  return [
    'get_wikis',
    'get_wiki_page',
    'create_wiki',
    'update_wiki_page',
    'list_wiki_pages',
    'create_wiki_page',
  ].includes(toolName);
};

/**
 * Handles wikis feature requests
 */
export const handleWikisRequest: RequestHandler = async (
  connection: WebApi,
  request: CallToolRequest,
): Promise<{ content: Array<{ type: string; text: string }> }> => {
  switch (request.params.name) {
    case 'get_wikis': {
      const args = GetWikisSchema.parse(request.params.arguments);
      const result = await getWikis(connection, {
        organizationId: args.organizationId ?? defaultOrg,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'get_wiki_page': {
      const args = GetWikiPageSchema.parse(request.params.arguments);
      const result = await getWikiPage({
        organizationId: args.organizationId ?? defaultOrg,
        projectId: args.projectId ?? defaultProject,
        wikiId: args.wikiId,
        pagePath: args.pagePath,
      });
      return {
        content: [{ type: 'text', text: result }],
      };
    }
    case 'create_wiki': {
      const args = CreateWikiSchema.parse(request.params.arguments);
      const result = await createWiki(connection, {
        organizationId: args.organizationId ?? defaultOrg,
        projectId: args.projectId ?? defaultProject,
        name: args.name,
        type: args.type,
        repositoryId: args.repositoryId ?? undefined,
        mappedPath: args.mappedPath ?? undefined,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'update_wiki_page': {
      const args = UpdateWikiPageSchema.parse(request.params.arguments);
      const result = await updateWikiPage({
        organizationId: args.organizationId ?? defaultOrg,
        projectId: args.projectId ?? defaultProject,
        wikiId: args.wikiId,
        pagePath: args.pagePath,
        content: args.content,
        comment: args.comment,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'list_wiki_pages': {
      const args = ListWikiPagesSchema.parse(request.params.arguments);
      const result = await listWikiPages({
        organizationId: args.organizationId ?? defaultOrg,
        projectId: args.projectId ?? defaultProject,
        wikiId: args.wikiId,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'create_wiki_page': {
      const args = CreateWikiPageSchema.parse(request.params.arguments);
      const result = await createWikiPage({
        organizationId: args.organizationId ?? defaultOrg,
        projectId: args.projectId ?? defaultProject,
        wikiId: args.wikiId,
        pagePath: args.pagePath,
        content: args.content,
        comment: args.comment,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    default:
      throw new Error(`Unknown wikis tool: ${request.params.name}`);
  }
};

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import {
  GitVersionDescriptor,
  GitItem,
  GitVersionType,
  VersionControlRecursionType,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { AzureDevOpsResourceNotFoundError } from '../../../shared/errors';

/**
 * Response format for file content
 */
export interface FileContentResponse {
  content: string;
  isDirectory: boolean;
}

/**
 * Get content of a file or directory from a repository
 *
 * @param connection - Azure DevOps WebApi connection
 * @param projectId - Project ID or name
 * @param repositoryId - Repository ID or name
 * @param path - Path to file or directory
 * @param versionDescriptor - Optional version descriptor for retrieving file at specific commit/branch/tag
 * @returns Content of the file or list of items if path is a directory
 */
export async function getFileContent(
  connection: WebApi,
  projectId: string,
  repositoryId: string,
  path: string = '/',
  versionDescriptor?: { versionType: GitVersionType; version: string },
): Promise<FileContentResponse> {
  try {
    const gitApi = await connection.getGitApi();

    // Create version descriptor for API requests
    const gitVersionDescriptor: GitVersionDescriptor | undefined =
      versionDescriptor
        ? {
            version: versionDescriptor.version,
            versionType: versionDescriptor.versionType,
            versionOptions: undefined,
          }
        : undefined;

    // First, try to get items using the path to determine if it's a directory
    let isDirectory = false;
    let items: GitItem[] = [];

    try {
      items = await gitApi.getItems(
        repositoryId,
        projectId,
        path,
        VersionControlRecursionType.OneLevel,
        undefined,
        undefined,
        undefined,
        undefined,
        gitVersionDescriptor,
      );

      // If multiple items are returned or the path ends with /, it's a directory
      isDirectory = items.length > 1 || (path !== '/' && path.endsWith('/'));
    } catch {
      // If getItems fails, try to get file content directly
      isDirectory = false;
    }

    if (isDirectory) {
      // For directories, return a formatted list of the items
      return {
        content: JSON.stringify(items, null, 2),
        isDirectory: true,
      };
    } else {
      // For files, get the actual content
      try {
        // Get file content using the Git API
        const contentStream = await gitApi.getItemContent(
          repositoryId,
          path,
          projectId,
          undefined,
          undefined,
          undefined,
          undefined,
          false,
          gitVersionDescriptor,
          true,
        );

        // Convert the stream to a string
        if (contentStream) {
          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
          const 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);
            });
          });

          return {
            content,
            isDirectory: false,
          };
        }

        throw new Error('No content returned from API');
      } catch (error) {
        // If it's a 404 or similar error, throw a ResourceNotFoundError
        if (
          error instanceof Error &&
          (error.message.includes('not found') ||
            error.message.includes('does not exist'))
        ) {
          throw new AzureDevOpsResourceNotFoundError(
            `Path '${path}' not found in repository '${repositoryId}' of project '${projectId}'`,
          );
        }
        throw error;
      }
    }
  } catch (error) {
    // If it's already an AzureDevOpsResourceNotFoundError, rethrow it
    if (error instanceof AzureDevOpsResourceNotFoundError) {
      throw error;
    }

    // Otherwise, wrap it in a ResourceNotFoundError
    throw new AzureDevOpsResourceNotFoundError(
      `Failed to get content for path '${path}': ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import axios from 'axios';
import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity';
import {
  AzureDevOpsError,
  AzureDevOpsAuthenticationError,
  AzureDevOpsValidationError,
} from '../../../shared/errors';
import { UserProfile } from '../types';

/**
 * Get details of the currently authenticated user
 *
 * This function returns basic profile information about the authenticated user.
 *
 * @param connection The Azure DevOps WebApi connection
 * @returns User profile information including id, displayName, and email
 * @throws {AzureDevOpsError} If retrieval of user information fails
 */
export async function getMe(connection: WebApi): Promise<UserProfile> {
  try {
    // Extract organization from the connection URL
    const { organization } = extractOrgFromUrl(connection.serverUrl);

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

    // Make direct call to the Profile API endpoint
    // Note: This API is in the vssps.dev.azure.com domain, not dev.azure.com
    const response = await axios.get(
      `https://vssps.dev.azure.com/${organization}/_apis/profile/profiles/me?api-version=7.1`,
      {
        headers: {
          Authorization: authHeader,
          'Content-Type': 'application/json',
        },
      },
    );

    const profile = response.data;

    // Return the user profile with required fields
    return {
      id: profile.id,
      displayName: profile.displayName || '',
      email: profile.emailAddress || '',
    };
  } catch (error) {
    // Handle authentication errors
    if (
      axios.isAxiosError(error) &&
      (error.response?.status === 401 || error.response?.status === 403)
    ) {
      throw new AzureDevOpsAuthenticationError(
        `Authentication failed: ${error.message}`,
      );
    }

    // If it's already an AzureDevOpsError, rethrow it
    if (error instanceof AzureDevOpsError) {
      throw error;
    }

    // Otherwise, wrap it in a generic error
    throw new AzureDevOpsError(
      `Failed to get user information: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

/**
 * Extract organization from the Azure DevOps URL
 *
 * @param url The Azure DevOps URL
 * @returns The organization
 */
function extractOrgFromUrl(url: string): { organization: string } {
  // First try modern dev.azure.com format
  let match = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)/);

  // If not found, try legacy visualstudio.com format
  if (!match) {
    match = url.match(/https?:\/\/([^.]+)\.visualstudio\.com/);
  }

  // Fallback: capture the first path segment for any URL
  if (!match) {
    match = url.match(/https?:\/\/[^/]+\/([^/]+)/);
  }

  const organization = match ? match[1] : '';

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

  return {
    organization,
  };
}

/**
 * Get the authorization header for API requests
 *
 * @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 AzureDevOpsAuthenticationError(
      `Failed to get authorization header: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import axios from 'axios';
import { searchWiki } from './feature';

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

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

describe('searchWiki unit', () => {
  // Mock WebApi connection
  const mockConnection = {
    _getHttpClient: jest.fn().mockReturnValue({
      getAuthorizationHeader: jest.fn().mockReturnValue('Bearer mock-token'),
    }),
    getCoreApi: jest.fn().mockImplementation(() => ({
      getProjects: jest
        .fn()
        .mockResolvedValue([{ name: 'TestProject', id: 'project-id' }]),
    })),
    serverUrl: 'https://dev.azure.com/testorg',
  } as unknown as WebApi;

  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('should return wiki search results with project ID', async () => {
    // Arrange
    const mockSearchResponse = {
      data: {
        count: 1,
        results: [
          {
            fileName: 'Example Page',
            path: '/Example Page',
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'TestProject',
              id: 'project-id',
            },
            hits: [
              {
                content: 'This is an example page',
                charOffset: 5,
                length: 7,
              },
            ],
          },
        ],
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

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

    // Assert
    expect(result).toBeDefined();
    expect(result.count).toBe(1);
    expect(result.results).toHaveLength(1);
    expect(result.results[0].fileName).toBe('Example Page');
    expect(mockedAxios.post).toHaveBeenCalledTimes(1);
    expect(mockedAxios.post).toHaveBeenCalledWith(
      expect.stringContaining(
        'https://almsearch.dev.azure.com/testorg/TestProject/_apis/search/wikisearchresults',
      ),
      expect.objectContaining({
        searchText: 'example',
        filters: expect.objectContaining({
          Project: ['TestProject'],
        }),
      }),
      expect.any(Object),
    );
  });

  test('should perform organization-wide wiki search when projectId is not provided', async () => {
    // Arrange
    const mockSearchResponse = {
      data: {
        count: 2,
        results: [
          {
            fileName: 'Example Page 1',
            path: '/Example Page 1',
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'Project1',
              id: 'project-id-1',
            },
            hits: [
              {
                content: 'This is an example page',
                charOffset: 5,
                length: 7,
              },
            ],
          },
          {
            fileName: 'Example Page 2',
            path: '/Example Page 2',
            collection: {
              name: 'DefaultCollection',
            },
            project: {
              name: 'Project2',
              id: 'project-id-2',
            },
            hits: [
              {
                content: 'This is another example page',
                charOffset: 5,
                length: 7,
              },
            ],
          },
        ],
      },
    };

    mockedAxios.post.mockResolvedValueOnce(mockSearchResponse);

    // Act
    const result = await searchWiki(mockConnection, {
      searchText: 'example',
    });

    // Assert
    expect(result).toBeDefined();
    expect(result.count).toBe(2);
    expect(result.results).toHaveLength(2);
    expect(result.results[0].project.name).toBe('Project1');
    expect(result.results[1].project.name).toBe('Project2');
    expect(mockedAxios.post).toHaveBeenCalledTimes(1);
    expect(mockedAxios.post).toHaveBeenCalledWith(
      expect.stringContaining(
        'https://almsearch.dev.azure.com/testorg/_apis/search/wikisearchresults',
      ),
      expect.not.objectContaining({
        filters: expect.objectContaining({
          Project: expect.anything(),
        }),
      }),
      expect.any(Object),
    );
  });
});

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import {
  Comment,
  CommentThreadStatus,
  CommentType,
  GitPullRequestCommentThread,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { AzureDevOpsError } from '../../../shared/errors';
import { AddPullRequestCommentOptions, AddCommentResponse } from '../types';
import {
  transformCommentThreadStatus,
  transformCommentType,
} from '../../../shared/enums';

/**
 * Add a comment to a pull request
 *
 * @param connection The Azure DevOps WebApi connection
 * @param projectId The ID or name of the project
 * @param repositoryId The ID or name of the repository
 * @param pullRequestId The ID of the pull request
 * @param options Options for adding the comment
 * @returns The created comment or thread
 */
export async function addPullRequestComment(
  connection: WebApi,
  projectId: string,
  repositoryId: string,
  pullRequestId: number,
  options: AddPullRequestCommentOptions,
): Promise<AddCommentResponse> {
  try {
    const gitApi = await connection.getGitApi();

    // Create comment object
    const comment: Comment = {
      content: options.content,
      commentType: CommentType.Text, // Default to Text type
      parentCommentId: options.parentCommentId,
    };

    // Case 1: Add comment to an existing thread
    if (options.threadId) {
      const createdComment = await gitApi.createComment(
        comment,
        repositoryId,
        pullRequestId,
        options.threadId,
        projectId,
      );

      if (!createdComment) {
        throw new Error('Failed to create pull request comment');
      }

      return {
        comment: {
          ...createdComment,
          commentType: transformCommentType(createdComment.commentType),
        },
      };
    }
    // Case 2: Create new thread with comment
    else {
      // Map status string to CommentThreadStatus enum
      let threadStatus: CommentThreadStatus | undefined;
      if (options.status) {
        switch (options.status) {
          case 'active':
            threadStatus = CommentThreadStatus.Active;
            break;
          case 'fixed':
            threadStatus = CommentThreadStatus.Fixed;
            break;
          case 'wontFix':
            threadStatus = CommentThreadStatus.WontFix;
            break;
          case 'closed':
            threadStatus = CommentThreadStatus.Closed;
            break;
          case 'pending':
            threadStatus = CommentThreadStatus.Pending;
            break;
          case 'byDesign':
            threadStatus = CommentThreadStatus.ByDesign;
            break;
          case 'unknown':
            threadStatus = CommentThreadStatus.Unknown;
            break;
        }
      }

      // Create thread with comment
      const thread: GitPullRequestCommentThread = {
        comments: [comment],
        status: threadStatus,
      };

      // Add file context if specified (file comment)
      if (options.filePath) {
        thread.threadContext = {
          filePath: options.filePath,
          // Only add line information if provided
          rightFileStart: options.lineNumber
            ? {
                line: options.lineNumber,
                offset: 1, // Default to start of line
              }
            : undefined,
          rightFileEnd: options.lineNumber
            ? {
                line: options.lineNumber,
                offset: 1, // Default to start of line
              }
            : undefined,
        };
      }

      const createdThread = await gitApi.createThread(
        thread,
        repositoryId,
        pullRequestId,
        projectId,
      );

      if (
        !createdThread ||
        !createdThread.comments ||
        createdThread.comments.length === 0
      ) {
        throw new Error('Failed to create pull request comment thread');
      }

      return {
        comment: {
          ...createdThread.comments[0],
          commentType: transformCommentType(
            createdThread.comments[0].commentType,
          ),
        },
        thread: {
          ...createdThread,
          status: transformCommentThreadStatus(createdThread.status),
          comments: createdThread.comments?.map((comment) => ({
            ...comment,
            commentType: transformCommentType(comment.commentType),
          })),
        },
      };
    }
  } catch (error) {
    if (error instanceof AzureDevOpsError) {
      throw error;
    }
    throw new Error(
      `Failed to add pull request comment: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

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

```typescript
import { manageWorkItemLink } from './feature';
import { AzureDevOpsResourceNotFoundError } from '../../../shared/errors';

describe('manageWorkItemLink', () => {
  let mockConnection: any;
  let mockWitApi: any;

  const projectId = 'test-project';
  const sourceWorkItemId = 123;
  const targetWorkItemId = 456;
  const relationType = 'System.LinkTypes.Related';
  const newRelationType = 'System.LinkTypes.Hierarchy-Forward';
  const comment = 'Test link comment';

  beforeEach(() => {
    mockWitApi = {
      updateWorkItem: jest.fn(),
    };

    mockConnection = {
      getWorkItemTrackingApi: jest.fn().mockResolvedValue(mockWitApi),
      serverUrl: 'https://dev.azure.com/test-org',
    };
  });

  test('should add a work item link', async () => {
    // Setup
    const updatedWorkItem = {
      id: sourceWorkItemId,
      fields: { 'System.Title': 'Test' },
    };
    mockWitApi.updateWorkItem.mockResolvedValue(updatedWorkItem);

    // Execute
    const result = await manageWorkItemLink(mockConnection, projectId, {
      sourceWorkItemId,
      targetWorkItemId,
      operation: 'add',
      relationType,
      comment,
    });

    // Verify
    expect(mockConnection.getWorkItemTrackingApi).toHaveBeenCalled();
    expect(mockWitApi.updateWorkItem).toHaveBeenCalledWith(
      {}, // customHeaders
      [
        {
          op: 'add',
          path: '/relations/-',
          value: {
            rel: relationType,
            url: `${mockConnection.serverUrl}/_apis/wit/workItems/${targetWorkItemId}`,
            attributes: { comment },
          },
        },
      ],
      sourceWorkItemId,
      projectId,
    );
    expect(result).toEqual(updatedWorkItem);
  });

  test('should remove a work item link', async () => {
    // Setup
    const updatedWorkItem = {
      id: sourceWorkItemId,
      fields: { 'System.Title': 'Test' },
    };
    mockWitApi.updateWorkItem.mockResolvedValue(updatedWorkItem);

    // Execute
    const result = await manageWorkItemLink(mockConnection, projectId, {
      sourceWorkItemId,
      targetWorkItemId,
      operation: 'remove',
      relationType,
    });

    // Verify
    expect(mockConnection.getWorkItemTrackingApi).toHaveBeenCalled();
    expect(mockWitApi.updateWorkItem).toHaveBeenCalledWith(
      {}, // customHeaders
      [
        {
          op: 'remove',
          path: `/relations/+[rel=${relationType};url=${mockConnection.serverUrl}/_apis/wit/workItems/${targetWorkItemId}]`,
        },
      ],
      sourceWorkItemId,
      projectId,
    );
    expect(result).toEqual(updatedWorkItem);
  });

  test('should update a work item link', async () => {
    // Setup
    const updatedWorkItem = {
      id: sourceWorkItemId,
      fields: { 'System.Title': 'Test' },
    };
    mockWitApi.updateWorkItem.mockResolvedValue(updatedWorkItem);

    // Execute
    const result = await manageWorkItemLink(mockConnection, projectId, {
      sourceWorkItemId,
      targetWorkItemId,
      operation: 'update',
      relationType,
      newRelationType,
      comment,
    });

    // Verify
    expect(mockConnection.getWorkItemTrackingApi).toHaveBeenCalled();
    expect(mockWitApi.updateWorkItem).toHaveBeenCalledWith(
      {}, // customHeaders
      [
        {
          op: 'remove',
          path: `/relations/+[rel=${relationType};url=${mockConnection.serverUrl}/_apis/wit/workItems/${targetWorkItemId}]`,
        },
        {
          op: 'add',
          path: '/relations/-',
          value: {
            rel: newRelationType,
            url: `${mockConnection.serverUrl}/_apis/wit/workItems/${targetWorkItemId}`,
            attributes: { comment },
          },
        },
      ],
      sourceWorkItemId,
      projectId,
    );
    expect(result).toEqual(updatedWorkItem);
  });

  test('should throw error when work item not found', async () => {
    // Setup
    mockWitApi.updateWorkItem.mockResolvedValue(null);

    // Execute and verify
    await expect(
      manageWorkItemLink(mockConnection, projectId, {
        sourceWorkItemId,
        targetWorkItemId,
        operation: 'add',
        relationType,
      }),
    ).rejects.toThrow(AzureDevOpsResourceNotFoundError);
  });

  test('should throw error when update operation missing newRelationType', async () => {
    // Execute and verify
    await expect(
      manageWorkItemLink(mockConnection, projectId, {
        sourceWorkItemId,
        targetWorkItemId,
        operation: 'update',
        relationType,
        // newRelationType is missing
      }),
    ).rejects.toThrow('New relation type is required for update operation');
  });
});

```

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

```typescript
import { createPullRequest } from './feature';
import { AzureDevOpsError } from '../../../shared/errors';

describe('createPullRequest unit', () => {
  // Test for required fields validation
  test('should throw error when title is not provided', async () => {
    // Arrange - mock connection, never used due to validation error
    const mockConnection: any = {
      getGitApi: jest.fn(),
    };

    // Act & Assert
    await expect(
      createPullRequest(mockConnection, 'TestProject', 'TestRepo', {
        title: '',
        sourceRefName: 'refs/heads/feature-branch',
        targetRefName: 'refs/heads/main',
      }),
    ).rejects.toThrow('Title is required');
  });

  test('should throw error when source branch is not provided', async () => {
    // Arrange - mock connection, never used due to validation error
    const mockConnection: any = {
      getGitApi: jest.fn(),
    };

    // Act & Assert
    await expect(
      createPullRequest(mockConnection, 'TestProject', 'TestRepo', {
        title: 'Test PR',
        sourceRefName: '',
        targetRefName: 'refs/heads/main',
      }),
    ).rejects.toThrow('Source branch is required');
  });

  test('should throw error when target branch is not provided', async () => {
    // Arrange - mock connection, never used due to validation error
    const mockConnection: any = {
      getGitApi: jest.fn(),
    };

    // Act & Assert
    await expect(
      createPullRequest(mockConnection, 'TestProject', 'TestRepo', {
        title: 'Test PR',
        sourceRefName: 'refs/heads/feature-branch',
        targetRefName: '',
      }),
    ).rejects.toThrow('Target branch is required');
  });

  // Test for error propagation
  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(
      createPullRequest(mockConnection, 'TestProject', 'TestRepo', {
        title: 'Test PR',
        sourceRefName: 'refs/heads/feature-branch',
        targetRefName: 'refs/heads/main',
      }),
    ).rejects.toThrow(AzureDevOpsError);

    await expect(
      createPullRequest(mockConnection, 'TestProject', 'TestRepo', {
        title: 'Test PR',
        sourceRefName: 'refs/heads/feature-branch',
        targetRefName: 'refs/heads/main',
      }),
    ).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(
      createPullRequest(mockConnection, 'TestProject', 'TestRepo', {
        title: 'Test PR',
        sourceRefName: 'refs/heads/feature-branch',
        targetRefName: 'refs/heads/main',
      }),
    ).rejects.toThrow('Failed to create pull request: Unexpected error');
  });

  test('should apply unique trimmed tags to the pull request', async () => {
    const createPullRequestMock = jest.fn().mockResolvedValue({
      pullRequestId: 99,
      labels: [{ name: 'existing' }],
    });
    const createPullRequestLabelMock = jest
      .fn()
      .mockImplementation(async (label: { name: string }) => ({
        name: label.name,
      }));

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue({
        createPullRequest: createPullRequestMock,
        createPullRequestLabel: createPullRequestLabelMock,
      }),
    };

    const result = await createPullRequest(
      mockConnection,
      'TestProject',
      'TestRepo',
      {
        title: 'Test PR',
        sourceRefName: 'refs/heads/feature-branch',
        targetRefName: 'refs/heads/main',
        tags: ['Tag-One', 'tag-one', ' Tag-Two ', ''],
      },
    );

    expect(createPullRequestMock).toHaveBeenCalledWith(
      expect.objectContaining({
        labels: [{ name: 'Tag-One' }, { name: 'Tag-Two' }],
      }),
      'TestRepo',
      'TestProject',
    );
    expect(createPullRequestLabelMock).toHaveBeenCalledTimes(2);
    expect(createPullRequestLabelMock).toHaveBeenCalledWith(
      { name: 'Tag-One' },
      'TestRepo',
      99,
      'TestProject',
    );
    expect(createPullRequestLabelMock).toHaveBeenCalledWith(
      { name: 'Tag-Two' },
      'TestRepo',
      99,
      'TestProject',
    );
    expect(result.labels).toEqual([
      { name: 'existing' },
      { name: 'Tag-One' },
      { name: 'Tag-Two' },
    ]);
  });
});

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { isProjectsRequest, handleProjectsRequest } from './index';
import { getProject } from './get-project';
import { getProjectDetails } from './get-project-details';
import { listProjects } from './list-projects';

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

jest.mock('./get-project-details', () => ({
  getProjectDetails: jest.fn(),
}));

jest.mock('./list-projects', () => ({
  listProjects: jest.fn(),
}));

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

  describe('isProjectsRequest', () => {
    it('should return true for projects requests', () => {
      const validTools = [
        'list_projects',
        'get_project',
        'get_project_details',
      ];
      validTools.forEach((tool) => {
        const request = {
          params: { name: tool, arguments: {} },
          method: 'tools/call',
        } as CallToolRequest;
        expect(isProjectsRequest(request)).toBe(true);
      });
    });

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

  describe('handleProjectsRequest', () => {
    it('should handle list_projects request', async () => {
      const mockProjects = [
        { id: '1', name: 'Project 1' },
        { id: '2', name: 'Project 2' },
      ];

      (listProjects as jest.Mock).mockResolvedValue(mockProjects);

      const request = {
        params: {
          name: 'list_projects',
          arguments: {
            top: 10,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handleProjectsRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockProjects,
      );
      expect(listProjects).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          top: 10,
        }),
      );
    });

    it('should handle get_project request', async () => {
      const mockProject = { id: '1', name: 'Project 1' };
      (getProject as jest.Mock).mockResolvedValue(mockProject);

      const request = {
        params: {
          name: 'get_project',
          arguments: {
            projectId: 'Project 1',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handleProjectsRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockProject,
      );
      expect(getProject).toHaveBeenCalledWith(mockConnection, 'Project 1');
    });

    it('should handle get_project_details request', async () => {
      const mockProjectDetails = {
        id: '1',
        name: 'Project 1',
        teams: [{ id: 'team1', name: 'Team 1' }],
      };

      (getProjectDetails as jest.Mock).mockResolvedValue(mockProjectDetails);

      const request = {
        params: {
          name: 'get_project_details',
          arguments: {
            projectId: 'Project 1',
            includeTeams: true,
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

      const response = await handleProjectsRequest(mockConnection, request);
      expect(response.content).toHaveLength(1);
      expect(JSON.parse(response.content[0].text as string)).toEqual(
        mockProjectDetails,
      );
      expect(getProjectDetails).toHaveBeenCalledWith(
        mockConnection,
        expect.objectContaining({
          projectId: 'Project 1',
          includeTeams: true,
        }),
      );
    });

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

      await expect(
        handleProjectsRequest(mockConnection, request),
      ).rejects.toThrow('Unknown projects tool');
    });

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

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

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

```

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

```typescript
/**
 * Base error class for Azure DevOps API errors.
 * All specific Azure DevOps errors should extend this class.
 *
 * @class AzureDevOpsError
 * @extends {Error}
 */
export class AzureDevOpsError extends Error {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
    this.name = 'AzureDevOpsError';
  }
}

/**
 * Error thrown when authentication with Azure DevOps fails.
 * This can occur due to invalid credentials, expired tokens, or network issues.
 *
 * @class AzureDevOpsAuthenticationError
 * @extends {AzureDevOpsError}
 */
export class AzureDevOpsAuthenticationError extends AzureDevOpsError {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
    this.name = 'AzureDevOpsAuthenticationError';
  }
}

/**
 * Type for API response error details
 */
export type ApiErrorResponse = {
  message?: string;
  statusCode?: number;
  details?: unknown;
  [key: string]: unknown;
};

/**
 * Error thrown when input validation fails.
 * This includes invalid parameters, malformed requests, or missing required fields.
 *
 * @class AzureDevOpsValidationError
 * @extends {AzureDevOpsError}
 * @property {ApiErrorResponse} [response] - The raw response from the API containing validation details
 */
export class AzureDevOpsValidationError extends AzureDevOpsError {
  response?: ApiErrorResponse;

  constructor(
    message: string,
    response?: ApiErrorResponse,
    options?: ErrorOptions,
  ) {
    super(message, options);
    this.name = 'AzureDevOpsValidationError';
    this.response = response;
  }
}

/**
 * Error thrown when a requested resource is not found.
 * This can occur when trying to access non-existent projects, repositories, or work items.
 *
 * @class AzureDevOpsResourceNotFoundError
 * @extends {AzureDevOpsError}
 */
export class AzureDevOpsResourceNotFoundError extends AzureDevOpsError {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
    this.name = 'AzureDevOpsResourceNotFoundError';
  }
}

/**
 * Error thrown when the user lacks permissions for an operation.
 * This occurs when trying to access or modify resources without proper authorization.
 *
 * @class AzureDevOpsPermissionError
 * @extends {AzureDevOpsError}
 */
export class AzureDevOpsPermissionError extends AzureDevOpsError {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
    this.name = 'AzureDevOpsPermissionError';
  }
}

/**
 * Error thrown when the API rate limit is exceeded.
 * Contains information about when the rate limit will reset.
 *
 * @class AzureDevOpsRateLimitError
 * @extends {AzureDevOpsError}
 * @property {Date} resetAt - The time when the rate limit will reset
 */
export class AzureDevOpsRateLimitError extends AzureDevOpsError {
  resetAt: Date;

  constructor(message: string, resetAt: Date, options?: ErrorOptions) {
    super(message, options);
    this.name = 'AzureDevOpsRateLimitError';
    this.resetAt = resetAt;
  }
}

/**
 * Helper function to check if an error is an Azure DevOps error.
 * Useful for type narrowing in catch blocks.
 *
 * @param {unknown} error - The error to check
 * @returns {boolean} True if the error is an Azure DevOps error
 *
 * @example
 * try {
 *   // Some Azure DevOps operation
 * } catch (error) {
 *   if (isAzureDevOpsError(error)) {
 *     // Handle Azure DevOps specific error
 *   } else {
 *     // Handle other errors
 *   }
 * }
 */
export function isAzureDevOpsError(error: unknown): error is AzureDevOpsError {
  return error instanceof AzureDevOpsError;
}

/**
 * Format an Azure DevOps error for display.
 * Provides a consistent error message format across different error types.
 *
 * @param {unknown} error - The error to format
 * @returns {string} A formatted error message
 *
 * @example
 * try {
 *   // Some Azure DevOps operation
 * } catch (error) {
 *   console.error(formatAzureDevOpsError(error));
 * }
 */
export function formatAzureDevOpsError(error: unknown): string {
  // Handle non-error objects
  if (error === null) {
    return 'null';
  }

  if (error === undefined) {
    return 'undefined';
  }

  if (typeof error === 'string') {
    return error;
  }

  if (typeof error === 'number' || typeof error === 'boolean') {
    return String(error);
  }

  // Handle error-like objects
  const errorObj = error as Record<string, unknown>;
  let message = `${errorObj.name || 'Unknown'}: ${errorObj.message || 'Unknown error'}`;

  if (error instanceof AzureDevOpsValidationError) {
    if (error.response) {
      message += `\nResponse: ${JSON.stringify(error.response)}`;
    } else {
      message += '\nNo response details available';
    }
  } else if (error instanceof AzureDevOpsRateLimitError) {
    message += `\nReset at: ${error.resetAt.toISOString()}`;
  }

  return message;
}

```

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

```markdown
# Azure DevOps Resource URIs

In addition to tools, the Azure DevOps MCP server provides access to resources via standardized URI patterns. Resources allow AI assistants to directly reference and retrieve content from Azure DevOps repositories using simple, predictable URLs.

## Repository Content Resources

The server supports accessing files and directories from Git repositories using the following resource URI patterns.

### Available Resource URI Templates

| Resource Type | URI Template | Description |
| ------------- | ------------ | ----------- |
| Default Branch Content | `ado://{organization}/{project}/{repo}/contents{/path*}` | Access file or directory content from the default branch |
| Branch-Specific Content | `ado://{organization}/{project}/{repo}/branches/{branch}/contents{/path*}` | Access content from a specific branch |
| Commit-Specific Content | `ado://{organization}/{project}/{repo}/commits/{commit}/contents{/path*}` | Access content from a specific commit |
| Tag-Specific Content | `ado://{organization}/{project}/{repo}/tags/{tag}/contents{/path*}` | Access content from a specific tag |
| Pull Request Content | `ado://{organization}/{project}/{repo}/pullrequests/{prId}/contents{/path*}` | Access content from a pull request |

### URI Components

- `{organization}`: Your Azure DevOps organization name
- `{project}`: The project name or ID
- `{repo}`: The repository name or ID
- `{path*}`: The path to the file or directory within the repository (optional)
- `{branch}`: The name of a branch
- `{commit}`: The SHA-1 hash of a commit
- `{tag}`: The name of a tag
- `{prId}`: The ID of a pull request

## Examples

### Accessing Files from the Default Branch

To access the content of a file in the default branch:

```
ado://myorg/MyProject/MyRepo/contents/src/index.ts
```

This retrieves the content of `index.ts` from the `src` directory in the default branch.

### Accessing Directory Content

To list the contents of a directory:

```
ado://myorg/MyProject/MyRepo/contents/src
```

This returns a JSON array containing information about all items in the `src` directory.

### Accessing Content from a Specific Branch

To access content from a feature branch:

```
ado://myorg/MyProject/MyRepo/branches/feature/new-ui/contents/src/index.ts
```

This retrieves the content of `index.ts` from the `feature/new-ui` branch.

### Accessing Content from a Specific Commit

To access content at a specific commit:

```
ado://myorg/MyProject/MyRepo/commits/a1b2c3d4e5f6g7h8i9j0/contents/src/index.ts
```

This retrieves the version of `index.ts` at the specified commit.

### Accessing Content from a Tag

To access content from a tagged release:

```
ado://myorg/MyProject/MyRepo/tags/v1.0.0/contents/README.md
```

This retrieves the README.md file from the v1.0.0 tag.

### Accessing Content from a Pull Request

To access content from a pull request:

```
ado://myorg/MyProject/MyRepo/pullrequests/42/contents/src/index.ts
```

This retrieves the version of `index.ts` from pull request #42.

## Implementation Details

When a resource URI is requested, the server:

1. Parses the URI to extract the components (organization, project, repository, path, etc.)
2. Establishes a connection to Azure DevOps using the configured authentication method
3. Determines if a specific version (branch, commit, tag) is requested
4. Uses the `getFileContent` functionality to retrieve the content
5. Returns the content with the appropriate MIME type

## Response Format

Responses are returned with the appropriate MIME type based on the file extension. For example:

- `.ts`, `.tsx` files: `application/typescript`
- `.js` files: `application/javascript`
- `.json` files: `application/json`
- `.md` files: `text/markdown`
- `.txt` files: `text/plain`
- `.html`, `.htm` files: `text/html`
- Image files (`.png`, `.jpg`, `.gif`, etc.): appropriate image MIME types

For directories, the content is returned as a JSON array with MIME type `application/json`.

## Error Handling

The resource handler may throw the following errors:

- `AzureDevOpsResourceNotFoundError`: If the specified resource cannot be found (project, repository, path, or version)
- `AzureDevOpsAuthenticationError`: If authentication fails
- `AzureDevOpsValidationError`: If the URI format is invalid
- Other errors: For unexpected issues

## Related Tools

While resource URIs provide direct access to repository content, you can also use the following tools for more advanced operations:

- `get_file_content`: Get content of a file or directory with more options and metadata
- `get_repository`: Get details about a specific repository
- `get_repository_details`: Get comprehensive repository information including statistics and refs
- `list_repositories`: List all repositories in a project
- `search_code`: Search for code in repositories 
```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { listWorkItems } from './feature';
import { createWorkItem } from '../create-work-item/feature';
import {
  getTestConnection,
  shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';
import { CreateWorkItemOptions, ListWorkItemsOptions } from '../types';

describe('listWorkItems integration', () => {
  let connection: WebApi | null = null;
  const createdWorkItemIds: number[] = [];
  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;
    }

    // Create a few work items to ensure we have data to list
    const testPrefix = `List Test ${new Date().toISOString().slice(0, 16)}`;

    for (let i = 0; i < 3; i++) {
      const options: CreateWorkItemOptions = {
        title: `${testPrefix} - Item ${i + 1}`,
        description: `Test item ${i + 1} for list-work-items integration tests`,
        priority: 2,
        additionalFields: {
          'System.Tags': 'ListTest,Integration',
        },
      };

      try {
        const workItem = await createWorkItem(
          connection,
          projectName,
          'Task',
          options,
        );
        if (workItem && workItem.id !== undefined) {
          createdWorkItemIds.push(workItem.id);
        }
      } catch (error) {
        console.error(`Failed to create test work item ${i + 1}:`, error);
      }
    }
  });

  test('should list work items from a project', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest() || !connection) {
      return;
    }

    const options: ListWorkItemsOptions = {
      projectId: projectName,
    };

    // Act - make an actual API call to Azure DevOps
    const result = await listWorkItems(connection, options);

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

    // Should have at least some work items (including our created ones)
    expect(result.length).toBeGreaterThan(0);

    // Check basic structure of returned work items
    const firstItem = result[0];
    expect(firstItem.id).toBeDefined();
    expect(firstItem.fields).toBeDefined();

    if (firstItem.fields) {
      expect(firstItem.fields['System.Title']).toBeDefined();
    }
  });

  test('should apply pagination options', async () => {
    // Skip if no connection is available
    if (shouldSkipIntegrationTest() || !connection) {
      return;
    }

    // First get all items to know the total count
    const allOptions: ListWorkItemsOptions = {
      projectId: projectName,
    };

    const allItems = await listWorkItems(connection, allOptions);

    // Then get with pagination
    const paginationOptions: ListWorkItemsOptions = {
      projectId: projectName,
      top: 2, // Only get first 2 items
    };

    const paginatedResult = await listWorkItems(connection, paginationOptions);

    // Assert on pagination
    expect(paginatedResult).toBeDefined();
    expect(paginatedResult.length).toBeLessThanOrEqual(2);

    // If we have more than 2 total items, pagination should have limited results
    if (allItems.length > 2) {
      expect(paginatedResult.length).toBe(2);
      expect(paginatedResult.length).toBeLessThan(allItems.length);
    }
  });

  test('should list work items with custom WIQL query', async () => {
    // Skip if no connection is available or if we didn't create any test items
    if (
      shouldSkipIntegrationTest() ||
      !connection ||
      createdWorkItemIds.length === 0
    ) {
      return;
    }

    // Create a more specific WIQL query that includes the IDs of our created work items
    const workItemIdList = createdWorkItemIds.join(',');
    const wiql = `SELECT [System.Id], [System.Title] FROM WorkItems WHERE [System.TeamProject] = '${projectName}' AND [System.Id] IN (${workItemIdList}) AND [System.Tags] CONTAINS 'ListTest' ORDER BY [System.Id]`;

    const options: ListWorkItemsOptions = {
      projectId: projectName,
      wiql,
    };

    // Act - make an actual API call to Azure DevOps
    const result = await listWorkItems(connection, options);

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

    // Should have found our test items with the ListTest tag
    expect(result.length).toBeGreaterThan(0);

    // At least one of our created items should be in the results
    const foundCreatedItem = result.some((item) =>
      createdWorkItemIds.includes(item.id || -1),
    );

    expect(foundCreatedItem).toBe(true);
  });
});

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import axios, { AxiosError } from 'axios';
import { getMe } from './feature';
import {
  AzureDevOpsError,
  AzureDevOpsAuthenticationError,
} from '@/shared/errors';

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

// Mock env variables
const originalEnv = process.env;

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

  beforeEach(() => {
    // Reset mocks
    jest.resetAllMocks();

    // Mock WebApi with a server URL
    mockConnection = {
      serverUrl: 'https://dev.azure.com/testorg',
    } as WebApi;

    // Mock environment variables for PAT authentication
    process.env = {
      ...originalEnv,
      AZURE_DEVOPS_AUTH_METHOD: 'pat',
      AZURE_DEVOPS_PAT: 'test-pat',
    };
  });

  afterEach(() => {
    // Restore original env
    process.env = originalEnv;
  });

  it('should return user profile with id, displayName, and email', async () => {
    // Arrange
    const mockProfile = {
      id: 'user-id-123',
      displayName: 'Test User',
      emailAddress: '[email protected]',
      coreRevision: 1647,
      timeStamp: '2023-01-01T00:00:00.000Z',
      revision: 1647,
    };

    // Mock axios get to return profile data
    mockAxios.get.mockResolvedValue({ data: mockProfile });

    // Act
    const result = await getMe(mockConnection);

    // Assert
    expect(mockAxios.get).toHaveBeenCalledWith(
      'https://vssps.dev.azure.com/testorg/_apis/profile/profiles/me?api-version=7.1',
      expect.any(Object),
    );

    expect(result).toEqual({
      id: 'user-id-123',
      displayName: 'Test User',
      email: '[email protected]',
    });
  });

  it('should handle missing email', async () => {
    // Arrange
    const mockProfile = {
      id: 'user-id-123',
      displayName: 'Test User',
      // No emailAddress
      coreRevision: 1647,
      timeStamp: '2023-01-01T00:00:00.000Z',
      revision: 1647,
    };

    // Mock axios get to return profile data
    mockAxios.get.mockResolvedValue({ data: mockProfile });

    // Act
    const result = await getMe(mockConnection);

    // Assert
    expect(result.email).toBe('');
  });

  it('should handle missing display name', async () => {
    // Arrange
    const mockProfile = {
      id: 'user-id-123',
      // No displayName
      emailAddress: '[email protected]',
      coreRevision: 1647,
      timeStamp: '2023-01-01T00:00:00.000Z',
      revision: 1647,
    };

    // Mock axios get to return profile data
    mockAxios.get.mockResolvedValue({ data: mockProfile });

    // Act
    const result = await getMe(mockConnection);

    // Assert
    expect(result.displayName).toBe('');
  });

  it('should handle authentication errors', async () => {
    // Arrange
    const axiosError = {
      isAxiosError: true,
      response: {
        status: 401,
        data: { message: 'Unauthorized' },
      },
      message: 'Request failed with status code 401',
    } as AxiosError;

    // Mock axios get to throw error
    mockAxios.get.mockRejectedValue(axiosError);

    // Mock axios.isAxiosError function
    jest.spyOn(axios, 'isAxiosError').mockImplementation(() => true);

    // Act & Assert
    await expect(getMe(mockConnection)).rejects.toThrow(
      AzureDevOpsAuthenticationError,
    );
    await expect(getMe(mockConnection)).rejects.toThrow(
      /Authentication failed/,
    );
  });

  it('should wrap general errors in AzureDevOpsError', async () => {
    // Arrange
    const testError = new Error('Test API error');
    mockAxios.get.mockRejectedValue(testError);

    // Mock axios.isAxiosError function
    jest.spyOn(axios, 'isAxiosError').mockImplementation(() => false);

    // Act & Assert
    await expect(getMe(mockConnection)).rejects.toThrow(AzureDevOpsError);
    await expect(getMe(mockConnection)).rejects.toThrow(
      'Failed to get user information: Test API error',
    );
  });

  // Test the legacy URL format of project.visualstudio.com
  it('should work with legacy visualstudio.com URL format', async () => {
    mockConnection = {
      serverUrl: 'https://legacy_test_org.visualstudio.com',
    } as WebApi;

    const mockProfile = {
      id: 'user-id-123',
      displayName: 'Test User',
      emailAddress: '[email protected]',
      coreRevision: 1647,
      timeStamp: '2023-01-01T00:00:00.000Z',
      revision: 1647,
    };

    mockAxios.get.mockResolvedValue({ data: mockProfile });

    const result = await getMe(mockConnection);

    // Verify that the organization name was correctly extracted from the legacy URL
    expect(mockAxios.get).toHaveBeenCalledWith(
      'https://vssps.dev.azure.com/legacy_test_org/_apis/profile/profiles/me?api-version=7.1',
      expect.any(Object),
    );

    expect(result).toEqual({
      id: 'user-id-123',
      displayName: 'Test User',
      email: '[email protected]',
    });
  });
});

```

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

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

/**
 * Schema for getting a work item
 */
export const GetWorkItemSchema = z.object({
  workItemId: z.number().describe('The ID of the work item'),
  expand: z
    .enum(['none', 'relations', 'fields', 'links', 'all'])
    .optional()
    .describe(
      'The level of detail to include in the response. Defaults to "all" if not specified.',
    ),
});

/**
 * Schema for listing work items
 */
export const ListWorkItemsSchema = 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})`),
  teamId: z.string().optional().describe('The ID of the team'),
  queryId: z.string().optional().describe('ID of a saved work item query'),
  wiql: z.string().optional().describe('Work Item Query Language (WIQL) query'),
  top: z.number().optional().describe('Maximum number of work items to return'),
  skip: z.number().optional().describe('Number of work items to skip'),
});

/**
 * Schema for creating a work item
 */
export const CreateWorkItemSchema = 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})`),
  workItemType: z
    .string()
    .describe(
      'The type of work item to create (e.g., "Task", "Bug", "User Story")',
    ),
  title: z.string().describe('The title of the work item'),
  description: z
    .string()
    .optional()
    .describe(
      'Work item description in HTML format. Multi-line text fields (i.e., System.History, AcceptanceCriteria, etc.) must use HTML format. Do not use CDATA tags.',
    ),
  assignedTo: z
    .string()
    .optional()
    .describe('The email or name of the user to assign the work item to'),
  areaPath: z.string().optional().describe('The area path for the work item'),
  iterationPath: z
    .string()
    .optional()
    .describe('The iteration path for the work item'),
  priority: z.number().optional().describe('The priority of the work item'),
  parentId: z
    .number()
    .optional()
    .describe('The ID of the parent work item to create a relationship with'),
  additionalFields: z
    .record(z.string(), z.any())
    .optional()
    .describe(
      'Additional fields to set on the work item. Multi-line text fields (i.e., System.History, AcceptanceCriteria, etc.) must use HTML format. Do not use CDATA tags.',
    ),
});

/**
 * Schema for updating a work item
 */
export const UpdateWorkItemSchema = z.object({
  workItemId: z.number().describe('The ID of the work item to update'),
  title: z.string().optional().describe('The updated title of the work item'),
  description: z
    .string()
    .optional()
    .describe(
      'Work item description in HTML format. Multi-line text fields (i.e., System.History, AcceptanceCriteria, etc.) must use HTML format. Do not use CDATA tags.',
    ),
  assignedTo: z
    .string()
    .optional()
    .describe('The email or name of the user to assign the work item to'),
  areaPath: z
    .string()
    .optional()
    .describe('The updated area path for the work item'),
  iterationPath: z
    .string()
    .optional()
    .describe('The updated iteration path for the work item'),
  priority: z
    .number()
    .optional()
    .describe('The updated priority of the work item'),
  state: z.string().optional().describe('The updated state of the work item'),
  additionalFields: z
    .record(z.string(), z.any())
    .optional()
    .describe(
      'Additional fields to update on the work item. Multi-line text fields (i.e., System.History, AcceptanceCriteria, etc.) must use HTML format. Do not use CDATA tags.',
    ),
});

/**
 * Schema for managing work item links
 */
export const ManageWorkItemLinkSchema = z.object({
  sourceWorkItemId: z.number().describe('The ID of the source work item'),
  targetWorkItemId: z.number().describe('The ID of the target work item'),
  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})`),
  operation: z
    .enum(['add', 'remove', 'update'])
    .describe('The operation to perform on the link'),
  relationType: z
    .string()
    .describe(
      'The reference name of the relation type (e.g., "System.LinkTypes.Hierarchy-Forward")',
    ),
  newRelationType: z
    .string()
    .optional()
    .describe('The new relation type to use when updating a link'),
  comment: z
    .string()
    .optional()
    .describe('Optional comment explaining the link'),
});

```

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

```typescript
import { getConnection } from '../../../server';
import { shouldSkipIntegrationTest } from '../../../shared/test/test-helpers';
import { getAllRepositoriesTree } from './feature';
import { AzureDevOpsConfig } from '../../../shared/types';
import { WebApi } from 'azure-devops-node-api';
import { AuthenticationMethod } from '../../../shared/auth';

// Skip tests if no PAT is available
const hasPat = process.env.AZURE_DEVOPS_PAT && process.env.AZURE_DEVOPS_ORG_URL;
const describeOrSkip = hasPat ? describe : describe.skip;

describeOrSkip('getAllRepositoriesTree (Integration)', () => {
  let connection: WebApi;
  let config: AzureDevOpsConfig;
  let projectId: string;
  let orgId: string;

  beforeAll(async () => {
    if (shouldSkipIntegrationTest()) {
      return;
    }

    // Configuration values
    config = {
      organizationUrl: process.env.AZURE_DEVOPS_ORG_URL || '',
      authMethod: AuthenticationMethod.PersonalAccessToken,
      personalAccessToken: process.env.AZURE_DEVOPS_PAT || '',
      defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT || '',
    };

    // Use test project - should be defined in .env file
    projectId =
      process.env.AZURE_DEVOPS_TEST_PROJECT_ID ||
      process.env.AZURE_DEVOPS_DEFAULT_PROJECT ||
      '';

    // Extract organization ID from URL
    const url = new URL(config.organizationUrl);
    const pathParts = url.pathname.split('/').filter(Boolean);
    orgId = pathParts[0] || '';

    // Get Azure DevOps connection
    connection = await getConnection(config);

    // Skip tests if no project ID is set
    if (!projectId) {
      console.warn('Skipping integration tests: No test project ID set');
    }
  }, 30000);

  // Skip all tests if integration tests are disabled
  beforeEach(() => {
    if (shouldSkipIntegrationTest()) {
      jest.resetAllMocks();
      return;
    }
  });

  it('should retrieve tree for all repositories with maximum depth (default)', async () => {
    // Skip test if no project ID or if integration tests are disabled
    if (shouldSkipIntegrationTest() || !projectId) {
      return;
    }

    const result = await getAllRepositoriesTree(connection, {
      organizationId: orgId,
      projectId: projectId,
      // depth defaults to 0 (unlimited)
    });

    expect(result).toBeDefined();
    expect(result.repositories).toBeDefined();
    expect(Array.isArray(result.repositories)).toBe(true);
    expect(result.repositories.length).toBeGreaterThan(0);

    // Check that at least one repository has a tree
    const repoWithTree = result.repositories.find((r) => r.tree.length > 0);
    expect(repoWithTree).toBeDefined();

    if (repoWithTree) {
      // Verify that deep nesting is included (finding items with level > 2)
      // Note: This might not always be true depending on repos, but there should be at least some nested items
      const deepItems = repoWithTree.tree.filter((item) => item.level > 2);
      expect(deepItems.length).toBeGreaterThan(0);

      // Verify stats are correct
      expect(repoWithTree.stats.directories).toBeGreaterThanOrEqual(0);
      expect(repoWithTree.stats.files).toBeGreaterThan(0);
      const dirCount = repoWithTree.tree.filter((item) => item.isFolder).length;
      const fileCount = repoWithTree.tree.filter(
        (item) => !item.isFolder,
      ).length;
      expect(repoWithTree.stats.directories).toBe(dirCount);
      expect(repoWithTree.stats.files).toBe(fileCount);
    }
  }, 60000); // Longer timeout because max depth can take time

  it('should retrieve tree for all repositories with limited depth (depth=1)', async () => {
    // Skip test if no project ID or if integration tests are disabled
    if (shouldSkipIntegrationTest() || !projectId) {
      return;
    }

    const result = await getAllRepositoriesTree(connection, {
      organizationId: orgId,
      projectId: projectId,
      depth: 1, // Only 1 level deep
    });

    expect(result).toBeDefined();
    expect(result.repositories).toBeDefined();
    expect(Array.isArray(result.repositories)).toBe(true);
    expect(result.repositories.length).toBeGreaterThan(0);

    // Check that at least one repository has a tree
    const repoWithTree = result.repositories.find((r) => r.tree.length > 0);
    expect(repoWithTree).toBeDefined();

    if (repoWithTree) {
      // Verify that only shallow nesting is included (all items should have level = 1)
      const allItemsLevel1 = repoWithTree.tree.every(
        (item) => item.level === 1,
      );
      expect(allItemsLevel1).toBe(true);

      // Verify stats are correct
      expect(repoWithTree.stats.directories).toBeGreaterThanOrEqual(0);
      expect(repoWithTree.stats.files).toBeGreaterThanOrEqual(0);
      const dirCount = repoWithTree.tree.filter((item) => item.isFolder).length;
      const fileCount = repoWithTree.tree.filter(
        (item) => !item.isFolder,
      ).length;
      expect(repoWithTree.stats.directories).toBe(dirCount);
      expect(repoWithTree.stats.files).toBe(fileCount);
    }
  }, 30000);
});

```

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

```typescript
import {
  CommentThreadStatus,
  CommentType,
  GitVersionType,
  PullRequestStatus,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import {
  commentThreadStatusMapper,
  commentTypeMapper,
  pullRequestStatusMapper,
  gitVersionTypeMapper,
} from './index';

describe('Enum Mappers', () => {
  describe('commentThreadStatusMapper', () => {
    it('should map string values to enum values correctly', () => {
      expect(commentThreadStatusMapper.toEnum('active')).toBe(
        CommentThreadStatus.Active,
      );
      expect(commentThreadStatusMapper.toEnum('fixed')).toBe(
        CommentThreadStatus.Fixed,
      );
      expect(commentThreadStatusMapper.toEnum('wontfix')).toBe(
        CommentThreadStatus.WontFix,
      );
      expect(commentThreadStatusMapper.toEnum('closed')).toBe(
        CommentThreadStatus.Closed,
      );
      expect(commentThreadStatusMapper.toEnum('bydesign')).toBe(
        CommentThreadStatus.ByDesign,
      );
      expect(commentThreadStatusMapper.toEnum('pending')).toBe(
        CommentThreadStatus.Pending,
      );
      expect(commentThreadStatusMapper.toEnum('unknown')).toBe(
        CommentThreadStatus.Unknown,
      );
    });

    it('should map enum values to string values correctly', () => {
      expect(
        commentThreadStatusMapper.toString(CommentThreadStatus.Active),
      ).toBe('active');
      expect(
        commentThreadStatusMapper.toString(CommentThreadStatus.Fixed),
      ).toBe('fixed');
      expect(
        commentThreadStatusMapper.toString(CommentThreadStatus.WontFix),
      ).toBe('wontfix');
      expect(
        commentThreadStatusMapper.toString(CommentThreadStatus.Closed),
      ).toBe('closed');
      expect(
        commentThreadStatusMapper.toString(CommentThreadStatus.ByDesign),
      ).toBe('bydesign');
      expect(
        commentThreadStatusMapper.toString(CommentThreadStatus.Pending),
      ).toBe('pending');
      expect(
        commentThreadStatusMapper.toString(CommentThreadStatus.Unknown),
      ).toBe('unknown');
    });

    it('should handle case insensitive string input', () => {
      expect(commentThreadStatusMapper.toEnum('ACTIVE')).toBe(
        CommentThreadStatus.Active,
      );
      expect(commentThreadStatusMapper.toEnum('Active')).toBe(
        CommentThreadStatus.Active,
      );
    });

    it('should return undefined for invalid string values', () => {
      expect(commentThreadStatusMapper.toEnum('invalid')).toBeUndefined();
    });

    it('should return default value for invalid enum values', () => {
      expect(commentThreadStatusMapper.toString(999)).toBe('unknown');
    });
  });

  describe('commentTypeMapper', () => {
    it('should map string values to enum values correctly', () => {
      expect(commentTypeMapper.toEnum('text')).toBe(CommentType.Text);
      expect(commentTypeMapper.toEnum('codechange')).toBe(
        CommentType.CodeChange,
      );
      expect(commentTypeMapper.toEnum('system')).toBe(CommentType.System);
      expect(commentTypeMapper.toEnum('unknown')).toBe(CommentType.Unknown);
    });

    it('should map enum values to string values correctly', () => {
      expect(commentTypeMapper.toString(CommentType.Text)).toBe('text');
      expect(commentTypeMapper.toString(CommentType.CodeChange)).toBe(
        'codechange',
      );
      expect(commentTypeMapper.toString(CommentType.System)).toBe('system');
      expect(commentTypeMapper.toString(CommentType.Unknown)).toBe('unknown');
    });
  });

  describe('pullRequestStatusMapper', () => {
    it('should map string values to enum values correctly', () => {
      expect(pullRequestStatusMapper.toEnum('active')).toBe(
        PullRequestStatus.Active,
      );
      expect(pullRequestStatusMapper.toEnum('abandoned')).toBe(
        PullRequestStatus.Abandoned,
      );
      expect(pullRequestStatusMapper.toEnum('completed')).toBe(
        PullRequestStatus.Completed,
      );
    });

    it('should map enum values to string values correctly', () => {
      expect(pullRequestStatusMapper.toString(PullRequestStatus.Active)).toBe(
        'active',
      );
      expect(
        pullRequestStatusMapper.toString(PullRequestStatus.Abandoned),
      ).toBe('abandoned');
      expect(
        pullRequestStatusMapper.toString(PullRequestStatus.Completed),
      ).toBe('completed');
    });
  });

  describe('gitVersionTypeMapper', () => {
    it('should map string values to enum values correctly', () => {
      expect(gitVersionTypeMapper.toEnum('branch')).toBe(GitVersionType.Branch);
      expect(gitVersionTypeMapper.toEnum('commit')).toBe(GitVersionType.Commit);
      expect(gitVersionTypeMapper.toEnum('tag')).toBe(GitVersionType.Tag);
    });

    it('should map enum values to string values correctly', () => {
      expect(gitVersionTypeMapper.toString(GitVersionType.Branch)).toBe(
        'branch',
      );
      expect(gitVersionTypeMapper.toString(GitVersionType.Commit)).toBe(
        'commit',
      );
      expect(gitVersionTypeMapper.toString(GitVersionType.Tag)).toBe('tag');
    });
  });
});

```

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

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

/**
 * Schema for searching code in Azure DevOps repositories
 */
export const SearchCodeSchema = z
  .object({
    searchText: z.string().describe('The text to search for'),
    organizationId: z
      .string()
      .optional()
      .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
    projectId: z
      .string()
      .optional()
      .describe(
        `The ID or name of the project to search in (Default: ${defaultProject}). If not provided, the default project will be used.`,
      ),
    filters: z
      .object({
        Repository: z
          .array(z.string())
          .optional()
          .describe('Filter by repository names'),
        Path: z.array(z.string()).optional().describe('Filter by file paths'),
        Branch: z
          .array(z.string())
          .optional()
          .describe('Filter by branch names'),
        CodeElement: z
          .array(z.string())
          .optional()
          .describe('Filter by code element types (function, class, etc.)'),
      })
      .optional()
      .describe('Optional filters to narrow search results'),
    top: z
      .number()
      .int()
      .min(1)
      .max(1000)
      .default(100)
      .describe('Number of results to return (default: 100, max: 1000)'),
    skip: z
      .number()
      .int()
      .min(0)
      .default(0)
      .describe('Number of results to skip for pagination (default: 0)'),
    includeSnippet: z
      .boolean()
      .default(true)
      .describe('Whether to include code snippets in results (default: true)'),
    includeContent: z
      .boolean()
      .default(true)
      .describe(
        'Whether to include full file content in results (default: true)',
      ),
  })
  .transform((data) => {
    return {
      ...data,
      organizationId: data.organizationId ?? defaultOrg,
      projectId: data.projectId ?? defaultProject,
    };
  });

/**
 * Schema for searching wiki pages in Azure DevOps projects
 */
export const SearchWikiSchema = z.object({
  searchText: z.string().describe('The text to search for in wikis'),
  organizationId: z
    .string()
    .optional()
    .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
  projectId: z
    .string()
    .optional()
    .describe(
      `The ID or name of the project to search in (Default: ${defaultProject}). If not provided, the default project will be used.`,
    ),
  filters: z
    .object({
      Project: z
        .array(z.string())
        .optional()
        .describe('Filter by project names'),
    })
    .optional()
    .describe('Optional filters to narrow search results'),
  top: z
    .number()
    .int()
    .min(1)
    .max(1000)
    .default(100)
    .describe('Number of results to return (default: 100, max: 1000)'),
  skip: z
    .number()
    .int()
    .min(0)
    .default(0)
    .describe('Number of results to skip for pagination (default: 0)'),
  includeFacets: z
    .boolean()
    .default(true)
    .describe('Whether to include faceting in results (default: true)'),
});

/**
 * Schema for searching work items in Azure DevOps projects
 */
export const SearchWorkItemsSchema = z.object({
  searchText: z.string().describe('The text to search for in work items'),
  organizationId: z
    .string()
    .optional()
    .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
  projectId: z
    .string()
    .optional()
    .describe(
      `The ID or name of the project to search in (Default: ${defaultProject}). If not provided, the default project will be used.`,
    ),
  filters: z
    .object({
      'System.TeamProject': z
        .array(z.string())
        .optional()
        .describe('Filter by project names'),
      'System.WorkItemType': z
        .array(z.string())
        .optional()
        .describe('Filter by work item types (Bug, Task, User Story, etc.)'),
      'System.State': z
        .array(z.string())
        .optional()
        .describe('Filter by work item states (New, Active, Closed, etc.)'),
      'System.AssignedTo': z
        .array(z.string())
        .optional()
        .describe('Filter by assigned users'),
      'System.AreaPath': z
        .array(z.string())
        .optional()
        .describe('Filter by area paths'),
    })
    .optional()
    .describe('Optional filters to narrow search results'),
  top: z
    .number()
    .int()
    .min(1)
    .max(1000)
    .default(100)
    .describe('Number of results to return (default: 100, max: 1000)'),
  skip: z
    .number()
    .int()
    .min(0)
    .default(0)
    .describe('Number of results to skip for pagination (default: 0)'),
  includeFacets: z
    .boolean()
    .default(true)
    .describe('Whether to include faceting in results (default: true)'),
  orderBy: z
    .array(
      z.object({
        field: z.string().describe('Field to sort by'),
        sortOrder: z.enum(['ASC', 'DESC']).describe('Sort order (ASC/DESC)'),
      }),
    )
    .optional()
    .describe('Options for sorting search results'),
});

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { isSearchRequest, handleSearchRequest } from './index';
import { searchCode } from './search-code';
import { searchWiki } from './search-wiki';
import { searchWorkItems } from './search-work-items';

// Mock the imported modules
jest.mock('./search-code', () => ({
  searchCode: jest.fn(),
}));

jest.mock('./search-wiki', () => ({
  searchWiki: jest.fn(),
}));

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

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

  describe('isSearchRequest', () => {
    it('should return true for search requests', () => {
      const validTools = ['search_code', 'search_wiki', 'search_work_items'];
      validTools.forEach((tool) => {
        const request = {
          params: { name: tool, arguments: {} },
          method: 'tools/call',
        } as CallToolRequest;
        expect(isSearchRequest(request)).toBe(true);
      });
    });

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

  describe('handleSearchRequest', () => {
    it('should handle search_code request', async () => {
      const mockSearchResults = {
        count: 2,
        results: [
          { fileName: 'file1.ts', path: '/path/to/file1.ts' },
          { fileName: 'file2.ts', path: '/path/to/file2.ts' },
        ],
      };
      (searchCode as jest.Mock).mockResolvedValue(mockSearchResults);

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

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

    it('should handle search_wiki request', async () => {
      const mockSearchResults = {
        count: 1,
        results: [{ title: 'Wiki Page', path: '/path/to/page' }],
      };
      (searchWiki as jest.Mock).mockResolvedValue(mockSearchResults);

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

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

    it('should handle search_work_items request', async () => {
      const mockSearchResults = {
        count: 2,
        results: [
          { id: 1, title: 'Bug 1' },
          { id: 2, title: 'Feature 2' },
        ],
      };
      (searchWorkItems as jest.Mock).mockResolvedValue(mockSearchResults);

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

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

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

      await expect(
        handleSearchRequest(mockConnection, request),
      ).rejects.toThrow('Unknown search tool');
    });

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

      const request = {
        params: {
          name: 'search_code',
          arguments: {
            searchText: 'function',
          },
        },
        method: 'tools/call',
      } as CallToolRequest;

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

```

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

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

// Re-export features
export * from './list-pipelines';
export * from './get-pipeline';
export * from './list-pipeline-runs';
export * from './get-pipeline-run';
export * from './download-pipeline-artifact';
export * from './pipeline-timeline';
export * from './get-pipeline-log';
export * from './trigger-pipeline';

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

import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { WebApi } from 'azure-devops-node-api';
import {
  RequestIdentifier,
  RequestHandler,
} from '../../shared/types/request-handler';
import { ListPipelinesSchema } from './list-pipelines';
import { GetPipelineSchema } from './get-pipeline';
import { ListPipelineRunsSchema } from './list-pipeline-runs';
import { GetPipelineRunSchema } from './get-pipeline-run';
import { DownloadPipelineArtifactSchema } from './download-pipeline-artifact';
import { GetPipelineTimelineSchema } from './pipeline-timeline';
import { GetPipelineLogSchema } from './get-pipeline-log';
import { TriggerPipelineSchema } from './trigger-pipeline';
import { listPipelines } from './list-pipelines';
import { getPipeline } from './get-pipeline';
import { listPipelineRuns } from './list-pipeline-runs';
import { getPipelineRun } from './get-pipeline-run';
import { downloadPipelineArtifact } from './download-pipeline-artifact';
import { getPipelineTimeline } from './pipeline-timeline';
import { getPipelineLog } from './get-pipeline-log';
import { triggerPipeline } from './trigger-pipeline';
import { defaultProject } from '../../utils/environment';

/**
 * Checks if the request is for the pipelines feature
 */
export const isPipelinesRequest: RequestIdentifier = (
  request: CallToolRequest,
): boolean => {
  const toolName = request.params.name;
  return [
    'list_pipelines',
    'get_pipeline',
    'list_pipeline_runs',
    'get_pipeline_run',
    'download_pipeline_artifact',
    'pipeline_timeline',
    'get_pipeline_log',
    'trigger_pipeline',
  ].includes(toolName);
};

/**
 * Handles pipelines feature requests
 */
export const handlePipelinesRequest: RequestHandler = async (
  connection: WebApi,
  request: CallToolRequest,
): Promise<{ content: Array<{ type: string; text: string }> }> => {
  switch (request.params.name) {
    case 'list_pipelines': {
      const args = ListPipelinesSchema.parse(request.params.arguments);
      const result = await listPipelines(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'get_pipeline': {
      const args = GetPipelineSchema.parse(request.params.arguments);
      const result = await getPipeline(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'list_pipeline_runs': {
      const args = ListPipelineRunsSchema.parse(request.params.arguments);
      const result = await listPipelineRuns(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'get_pipeline_run': {
      const args = GetPipelineRunSchema.parse(request.params.arguments);
      const result = await getPipelineRun(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'download_pipeline_artifact': {
      const args = DownloadPipelineArtifactSchema.parse(
        request.params.arguments,
      );
      const result = await downloadPipelineArtifact(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'pipeline_timeline': {
      const args = GetPipelineTimelineSchema.parse(request.params.arguments);
      const result = await getPipelineTimeline(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'get_pipeline_log': {
      const args = GetPipelineLogSchema.parse(request.params.arguments);
      const result = await getPipelineLog(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      const text =
        typeof result === 'string' ? result : JSON.stringify(result, null, 2);
      return {
        content: [{ type: 'text', text }],
      };
    }
    case 'trigger_pipeline': {
      const args = TriggerPipelineSchema.parse(request.params.arguments);
      const result = await triggerPipeline(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    default:
      throw new Error(`Unknown pipelines tool: ${request.params.name}`);
  }
};

```

--------------------------------------------------------------------------------
/src/features/repositories/create-commit/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { Readable } from 'stream';
import { createCommit } from './feature';
import { AzureDevOpsError } from '../../../shared/errors';
import { createTwoFilesPatch } from 'diff';
import { VersionControlChangeType } from 'azure-devops-node-api/interfaces/GitInterfaces';

describe('createCommit unit', () => {
  test('should create push with provided changes', async () => {
    const createPush = jest.fn().mockResolvedValue({});
    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue({
        getBranch: jest
          .fn()
          .mockResolvedValue({ commit: { commitId: 'base' } }),
        getItemContent: jest
          .fn()
          .mockResolvedValue(Readable.from(['console.log("hello");\n'])),
        createPush,
      }),
    };

    await createCommit(mockConnection, {
      projectId: 'p',
      repositoryId: 'r',
      branchName: 'main',
      commitMessage: 'msg',
      changes: [
        {
          path: '/file.ts',
          patch: createTwoFilesPatch(
            '/file.ts',
            '/file.ts',
            'console.log("hello");\n',
            'console.log("world");\n',
          ),
        },
        {
          path: '/new.txt',
          patch: createTwoFilesPatch('/dev/null', '/new.txt', '', 'hi\n'),
        },
      ],
    });

    expect(createPush).toHaveBeenCalled();
    const payload = createPush.mock.calls[0][0];
    expect(payload.commits[0].changes).toHaveLength(2);
  });

  test('should throw when snippet not found', async () => {
    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue({
        getBranch: jest
          .fn()
          .mockResolvedValue({ commit: { commitId: 'base' } }),
        getItemContent: jest
          .fn()
          .mockResolvedValue(Readable.from(['nothing here'])),
      }),
    };

    await expect(
      createCommit(mockConnection, {
        projectId: 'p',
        repositoryId: 'r',
        branchName: 'main',
        commitMessage: 'msg',
        changes: [
          {
            path: '/file.ts',
            patch: createTwoFilesPatch(
              '/file.ts',
              '/file.ts',
              'console.log("hello");\n',
              'console.log("world");\n',
            ),
          },
        ],
      }),
    ).rejects.toThrow(AzureDevOpsError);
  });

  test('should create delete change when patch removes a file', async () => {
    const createPush = jest.fn().mockResolvedValue({});
    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue({
        getBranch: jest
          .fn()
          .mockResolvedValue({ commit: { commitId: 'base' } }),
        getItemContent: jest
          .fn()
          .mockResolvedValue(Readable.from(['goodbye\n'])),
        createPush,
      }),
    };

    await createCommit(mockConnection, {
      projectId: 'p',
      repositoryId: 'r',
      branchName: 'main',
      commitMessage: 'msg',
      changes: [
        {
          patch: createTwoFilesPatch('/old.txt', '/dev/null', 'goodbye\n', ''),
        },
      ],
    });

    expect(createPush).toHaveBeenCalled();
    const payload = createPush.mock.calls[0][0];
    const change = payload.commits[0].changes[0];
    expect(change.changeType).toBe(VersionControlChangeType.Delete);
    expect(change.item).toEqual({ path: '/old.txt' });
    expect(change.newContent).toBeUndefined();
  });

  test('should handle search/replace format', async () => {
    const createPush = jest.fn().mockResolvedValue({});
    const fileContent = 'const x = 1;\nconsole.log("hello");\n';
    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue({
        getBranch: jest
          .fn()
          .mockResolvedValue({ commit: { commitId: 'base' } }),
        getItemContent: jest
          .fn()
          .mockImplementation(() => Readable.from([fileContent])),
        createPush,
      }),
    };

    await createCommit(mockConnection, {
      projectId: 'p',
      repositoryId: 'r',
      branchName: 'main',
      commitMessage: 'msg',
      changes: [
        {
          path: '/file.ts',
          search: 'console.log("hello");',
          replace: 'console.log("world");',
        },
      ],
    });

    expect(createPush).toHaveBeenCalled();
    const payload = createPush.mock.calls[0][0];
    expect(payload.commits[0].changes).toHaveLength(1);
    const change = payload.commits[0].changes[0];
    expect(change.changeType).toBe(VersionControlChangeType.Edit);
    expect(change.newContent?.content).toContain('console.log("world");');
  });

  test('should throw when search string not found', async () => {
    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue({
        getBranch: jest
          .fn()
          .mockResolvedValue({ commit: { commitId: 'base' } }),
        getItemContent: jest
          .fn()
          .mockResolvedValue(Readable.from(['const x = 1;\n'])),
      }),
    };

    await expect(
      createCommit(mockConnection, {
        projectId: 'p',
        repositoryId: 'r',
        branchName: 'main',
        commitMessage: 'msg',
        changes: [
          {
            path: '/file.ts',
            search: 'NOT_FOUND',
            replace: 'something',
          },
        ],
      }),
    ).rejects.toThrow(AzureDevOpsError);
  });
});

```

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

```typescript
import { WebApi, getPersonalAccessTokenHandler } from 'azure-devops-node-api';
import { BearerCredentialHandler } from 'azure-devops-node-api/handlers/bearertoken';
import { DefaultAzureCredential, AzureCliCredential } from '@azure/identity';
import { AzureDevOpsAuthenticationError } from '../errors';

/**
 * Authentication methods supported by the Azure DevOps client
 */
export enum AuthenticationMethod {
  /**
   * Personal Access Token authentication
   */
  PersonalAccessToken = 'pat',

  /**
   * Azure Identity authentication (DefaultAzureCredential)
   */
  AzureIdentity = 'azure-identity',

  /**
   * Azure CLI authentication (AzureCliCredential)
   */
  AzureCli = 'azure-cli',
}

/**
 * Authentication configuration for Azure DevOps
 */
export interface AuthConfig {
  /**
   * Authentication method to use
   */
  method: AuthenticationMethod;

  /**
   * Organization URL (e.g., https://dev.azure.com/myorg)
   */
  organizationUrl: string;

  /**
   * Personal Access Token for Azure DevOps (required for PAT authentication)
   */
  personalAccessToken?: string;
}

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

/**
 * Creates an authenticated client for Azure DevOps API based on the specified authentication method
 *
 * @param config Authentication configuration
 * @returns Authenticated WebApi client
 * @throws {AzureDevOpsAuthenticationError} If authentication fails
 */
export async function createAuthClient(config: AuthConfig): Promise<WebApi> {
  if (!config.organizationUrl) {
    throw new AzureDevOpsAuthenticationError('Organization URL is required');
  }

  try {
    let client: WebApi;

    switch (config.method) {
      case AuthenticationMethod.PersonalAccessToken:
        client = await createPatClient(config);
        break;
      case AuthenticationMethod.AzureIdentity:
        client = await createAzureIdentityClient(config);
        break;
      case AuthenticationMethod.AzureCli:
        client = await createAzureCliClient(config);
        break;
      default:
        throw new AzureDevOpsAuthenticationError(
          `Unsupported authentication method: ${config.method}`,
        );
    }

    // Test the connection
    const locationsApi = await client.getLocationsApi();
    await locationsApi.getResourceAreas();

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

/**
 * Creates a client using Personal Access Token authentication
 *
 * @param config Authentication configuration
 * @returns Authenticated WebApi client
 * @throws {AzureDevOpsAuthenticationError} If PAT is missing or authentication fails
 */
async function createPatClient(config: AuthConfig): Promise<WebApi> {
  if (!config.personalAccessToken) {
    throw new AzureDevOpsAuthenticationError(
      'Personal Access Token is required',
    );
  }

  // Create authentication handler using PAT
  const authHandler = getPersonalAccessTokenHandler(config.personalAccessToken);

  // Create API client with the auth handler
  return new WebApi(config.organizationUrl, authHandler);
}

/**
 * Creates a client using DefaultAzureCredential authentication
 *
 * @param config Authentication configuration
 * @returns Authenticated WebApi client
 * @throws {AzureDevOpsAuthenticationError} If token acquisition fails
 */
async function createAzureIdentityClient(config: AuthConfig): Promise<WebApi> {
  try {
    // Create DefaultAzureCredential
    const credential = new DefaultAzureCredential();

    // 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');
    }

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

    // Create API client with the auth handler
    return new WebApi(config.organizationUrl, authHandler);
  } catch (error) {
    throw new AzureDevOpsAuthenticationError(
      `Failed to acquire Azure Identity token: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

/**
 * Creates a client using AzureCliCredential authentication
 *
 * @param config Authentication configuration
 * @returns Authenticated WebApi client
 * @throws {AzureDevOpsAuthenticationError} If token acquisition fails
 */
async function createAzureCliClient(config: AuthConfig): Promise<WebApi> {
  try {
    // Create AzureCliCredential
    const credential = new AzureCliCredential();

    // 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');
    }

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

    // Create API client with the auth handler
    return new WebApi(config.organizationUrl, authHandler);
  } catch (error) {
    throw new AzureDevOpsAuthenticationError(
      `Failed to acquire Azure CLI token: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

--------------------------------------------------------------------------------
/src/features/pipelines/list-pipeline-runs/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import {
  RunResult,
  RunState,
} from 'azure-devops-node-api/interfaces/PipelinesInterfaces';
import { listPipelineRuns } from './feature';
import {
  AzureDevOpsAuthenticationError,
  AzureDevOpsError,
  AzureDevOpsResourceNotFoundError,
} from '../../../shared/errors';
import { Run } from '../types';

describe('listPipelineRuns unit', () => {
  let mockConnection: WebApi;
  let mockPipelinesApi: any;
  let mockRestGet: jest.Mock;

  const sampleRuns: Run[] = [
    {
      id: 101,
      name: 'Run 101',
      createdDate: new Date('2024-01-01T10:00:00Z'),
      state: RunState.Completed,
      result: RunResult.Succeeded,
      url: 'https://dev.azure.com/org/project/_apis/pipelines/42/runs/101',
      _links: {
        web: { href: 'https://dev.azure.com/org/project/pipelines/run/101' },
      },
    },
  ];

  beforeEach(() => {
    jest.resetAllMocks();

    mockRestGet = jest.fn();

    mockPipelinesApi = {
      rest: { get: mockRestGet },
      createRequestOptions: jest
        .fn()
        .mockReturnValue({ acceptHeader: 'application/json' }),
      formatResponse: jest.fn().mockImplementation((result: any) => {
        if (!result) {
          return [];
        }
        return result.value ?? [];
      }),
    };

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

  it('returns runs with continuation token from headers', async () => {
    mockRestGet.mockResolvedValue({
      statusCode: 200,
      result: { value: sampleRuns },
      headers: { 'x-ms-continuationtoken': 'token-from-header' },
    });

    const result = await listPipelineRuns(mockConnection, {
      projectId: 'test-project',
      pipelineId: 42,
    });

    expect(mockConnection.getPipelinesApi).toHaveBeenCalled();
    expect(mockRestGet).toHaveBeenCalled();
    expect(result.runs).toEqual(sampleRuns);
    expect(result.continuationToken).toBe('token-from-header');

    const [requestUrl] = mockRestGet.mock.calls[0];
    const url = new URL(requestUrl);
    expect(url.searchParams.get('api-version')).toBe('7.1');
    expect(url.searchParams.get('$top')).toBe('50');
  });

  it('applies filters, pagination, and branch normalization', async () => {
    mockRestGet.mockResolvedValue({
      statusCode: 200,
      result: { value: [], continuationToken: 'body-token' },
      headers: {},
    });

    const createdFrom = '2024-01-01T00:00:00Z';
    const createdTo = '2024-01-31T23:59:59Z';

    await listPipelineRuns(mockConnection, {
      projectId: 'test-project',
      pipelineId: 42,
      top: 20,
      continuationToken: 'next-token',
      branch: 'main',
      state: 'completed',
      result: 'succeeded',
      createdFrom,
      createdTo,
      orderBy: 'createdDate asc',
    });

    const [requestUrl] = mockRestGet.mock.calls[0];
    const url = new URL(requestUrl);

    expect(url.searchParams.get('$top')).toBe('20');
    expect(url.searchParams.get('continuationToken')).toBe('next-token');
    expect(url.searchParams.get('branch')).toBe('refs/heads/main');
    expect(url.searchParams.get('state')).toBe('completed');
    expect(url.searchParams.get('result')).toBe('succeeded');
    expect(url.searchParams.get('createdDate/min')).toBe(createdFrom);
    expect(url.searchParams.get('createdDate/max')).toBe(createdTo);
    expect(url.searchParams.get('orderBy')).toBe('createdDate asc');
  });

  it('clamps top to 100 and preserves ref-prefixed branches', async () => {
    mockRestGet.mockResolvedValue({
      statusCode: 200,
      result: { value: [] },
      headers: {},
    });

    await listPipelineRuns(mockConnection, {
      projectId: 'test-project',
      pipelineId: 42,
      top: 150,
      branch: 'refs/heads/develop',
    });

    const [requestUrl] = mockRestGet.mock.calls[0];
    const url = new URL(requestUrl);

    expect(url.searchParams.get('$top')).toBe('100');
    expect(url.searchParams.get('branch')).toBe('refs/heads/develop');
  });

  it('extracts continuation token from body when header missing', async () => {
    mockRestGet.mockResolvedValue({
      statusCode: 200,
      result: { value: [], continuationToken: 'body-token' },
      headers: {},
    });

    const result = await listPipelineRuns(mockConnection, {
      projectId: 'test-project',
      pipelineId: 42,
    });

    expect(result.continuationToken).toBe('body-token');
  });

  it('throws resource not found when API returns 404', async () => {
    mockRestGet.mockResolvedValue({
      statusCode: 404,
      result: null,
      headers: {},
    });

    await expect(
      listPipelineRuns(mockConnection, {
        projectId: 'test-project',
        pipelineId: 999,
      }),
    ).rejects.toBeInstanceOf(AzureDevOpsResourceNotFoundError);
  });

  it('maps authentication errors', async () => {
    mockRestGet.mockRejectedValue(new Error('401 Unauthorized'));

    await expect(
      listPipelineRuns(mockConnection, {
        projectId: 'test-project',
        pipelineId: 42,
      }),
    ).rejects.toBeInstanceOf(AzureDevOpsAuthenticationError);
  });

  it('wraps unexpected errors', async () => {
    mockRestGet.mockRejectedValue(new Error('Boom'));

    await expect(
      listPipelineRuns(mockConnection, {
        projectId: 'test-project',
        pipelineId: 42,
      }),
    ).rejects.toBeInstanceOf(AzureDevOpsError);
  });
});

```

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

```typescript
import { getPullRequestChecks } from './feature';
import { GitStatusState } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { PolicyEvaluationStatus } from 'azure-devops-node-api/interfaces/PolicyInterfaces';
import { AzureDevOpsError } from '../../../shared/errors';

describe('getPullRequestChecks', () => {
  it('returns status checks and policy evaluations with pipeline references', async () => {
    const statusRecords: any[] = [
      {
        id: 10,
        state: GitStatusState.Failed,
        description: 'CI build',
        context: { name: 'CI', genre: 'continuous-integration' },
        targetUrl:
          'https://dev.azure.com/org/project/_apis/pipelines/55/runs/123?view=results',
        creationDate: new Date('2024-01-01T00:00:00Z'),
        updatedDate: new Date('2024-01-01T01:00:00Z'),
      },
      {
        id: 11,
        state: GitStatusState.Succeeded,
        description: 'Lint checks',
        context: { name: 'Lint', genre: 'validation' },
        targetUrl:
          'https://dev.azure.com/org/project/_build/results?buildId=456&definitionId=789',
        creationDate: new Date('2024-02-01T00:00:00Z'),
        updatedDate: new Date('2024-02-01T01:00:00Z'),
      },
    ];

    const evaluationRecords: any[] = [
      {
        evaluationId: 'eval-1',
        status: PolicyEvaluationStatus.Rejected,
        configuration: {
          id: 7,
          revision: 3,
          isBlocking: true,
          isEnabled: true,
          type: {
            id: 'policy-guid',
            displayName: 'Build',
          },
          settings: {
            displayName: 'CI Build',
            buildDefinitionId: 987,
          },
        },
        context: {
          buildId: 456,
          targetUrl:
            'https://dev.azure.com/org/project/_build/results?buildId=456',
          message: 'Build failed',
        },
      },
    ];

    const gitApi = {
      getPullRequestStatuses: jest.fn().mockResolvedValue(statusRecords),
    };
    const policyApi = {
      getPolicyEvaluations: jest.fn().mockResolvedValue(evaluationRecords),
    };
    const getProject = jest
      .fn()
      .mockResolvedValue({ id: 'project-guid', name: 'project' });

    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue(gitApi),
      getPolicyApi: jest.fn().mockResolvedValue(policyApi),
      getCoreApi: jest.fn().mockResolvedValue({ getProject }),
    };

    const result = await getPullRequestChecks(mockConnection, {
      projectId: 'project',
      repositoryId: 'repo',
      pullRequestId: 42,
    });

    expect(result.statuses).toHaveLength(2);
    expect(result.statuses[0].state).toBe('failed');
    expect(result.statuses[0].pipeline?.pipelineId).toBe(55);
    expect(result.statuses[0].pipeline?.runId).toBe(123);
    expect(result.statuses[1].pipeline?.buildId).toBe(456);
    expect(result.statuses[1].pipeline?.definitionId).toBe(789);

    expect(result.policyEvaluations).toHaveLength(1);
    expect(result.policyEvaluations[0].status).toBe('rejected');
    expect(result.policyEvaluations[0].pipeline?.definitionId).toBe(987);
    expect(result.policyEvaluations[0].pipeline?.buildId).toBe(456);
    expect(result.policyEvaluations[0].targetUrl).toContain('buildId=456');

    expect(getProject).toHaveBeenCalledWith('project');
    expect(gitApi.getPullRequestStatuses).toHaveBeenCalledWith(
      'repo',
      42,
      'project-guid',
    );
    expect(policyApi.getPolicyEvaluations).toHaveBeenCalledWith(
      'project-guid',
      'vstfs:///CodeReview/CodeReviewId/project-guid/42',
    );
  });

  it('re-throws Azure DevOps errors', async () => {
    const azureError = new AzureDevOpsError('Azure failure');
    const mockConnection: any = {
      getGitApi: jest.fn().mockRejectedValue(azureError),
      getPolicyApi: jest.fn(),
      getCoreApi: jest.fn().mockResolvedValue({
        getProject: jest.fn().mockResolvedValue({ id: 'project-guid' }),
      }),
    };

    await expect(
      getPullRequestChecks(mockConnection, {
        projectId: 'project',
        repositoryId: 'repo',
        pullRequestId: 1,
      }),
    ).rejects.toBe(azureError);
  });

  it('wraps unexpected errors', async () => {
    const mockConnection: any = {
      getGitApi: jest.fn().mockRejectedValue(new Error('boom')),
      getPolicyApi: jest.fn(),
      getCoreApi: jest.fn().mockResolvedValue({
        getProject: jest.fn().mockResolvedValue({ id: 'project-guid' }),
      }),
    };

    await expect(
      getPullRequestChecks(mockConnection, {
        projectId: 'project',
        repositoryId: 'repo',
        pullRequestId: 1,
      }),
    ).rejects.toThrow('Failed to get pull request checks: boom');
  });

  it('uses the provided project GUID without fetching project metadata', async () => {
    const mockConnection: any = {
      getGitApi: jest.fn().mockResolvedValue({
        getPullRequestStatuses: jest.fn().mockResolvedValue([]),
      }),
      getPolicyApi: jest.fn().mockResolvedValue({
        getPolicyEvaluations: jest.fn().mockResolvedValue([]),
      }),
      getCoreApi: jest.fn(() => {
        throw new Error('should not fetch project');
      }),
    };

    await expect(
      getPullRequestChecks(mockConnection, {
        projectId: '12345678-1234-1234-1234-1234567890ab',
        repositoryId: 'repo',
        pullRequestId: 1,
      }),
    ).resolves.toEqual({ statuses: [], policyEvaluations: [] });

    expect(mockConnection.getGitApi).toHaveBeenCalled();
    expect(mockConnection.getPolicyApi).toHaveBeenCalled();
  });
});

```

--------------------------------------------------------------------------------
/src/features/pipelines/get-pipeline-run/feature.spec.unit.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import {
  RunResult,
  RunState,
} from 'azure-devops-node-api/interfaces/PipelinesInterfaces';
import { getPipelineRun } from './feature';
import {
  AzureDevOpsAuthenticationError,
  AzureDevOpsError,
  AzureDevOpsResourceNotFoundError,
} from '../../../shared/errors';
import { Run } from '../types';

describe('getPipelineRun unit', () => {
  let mockConnection: WebApi;
  let mockPipelinesApi: any;
  let mockRestGet: jest.Mock;
  let mockBuildApi: any;

  const baseRun: Run = {
    id: 200,
    name: 'Run 200',
    createdDate: new Date('2024-02-01T10:00:00Z'),
    state: RunState.Completed,
    result: RunResult.Succeeded,
    url: 'https://dev.azure.com/org/project/_apis/pipelines/runs/200',
    _links: {
      web: { href: 'https://dev.azure.com/org/project/pipelines/run/200' },
    },
    pipeline: { id: 42 },
  };

  beforeEach(() => {
    jest.resetAllMocks();

    mockRestGet = jest.fn();

    mockPipelinesApi = {
      rest: { get: mockRestGet },
      createRequestOptions: jest
        .fn()
        .mockReturnValue({ acceptHeader: 'application/json' }),
      formatResponse: jest.fn().mockImplementation((result: any) => result),
    };

    mockBuildApi = {
      getBuild: jest.fn().mockRejectedValue(new Error('not found')),
    };

    mockConnection = {
      serverUrl: 'https://dev.azure.com/testorg',
      getPipelinesApi: jest.fn().mockResolvedValue(mockPipelinesApi),
      getBuildApi: jest.fn().mockResolvedValue(mockBuildApi),
    } as unknown as WebApi;
  });

  it('returns run details', async () => {
    mockRestGet.mockResolvedValue({
      statusCode: 200,
      result: baseRun,
      headers: {},
    });

    const run = await getPipelineRun(mockConnection, {
      projectId: 'test-project',
      runId: 200,
    });

    expect(run).toEqual(baseRun);
    expect(mockRestGet).toHaveBeenCalled();
    const [requestUrl] = mockRestGet.mock.calls[0];
    expect(requestUrl).toContain('/_apis/pipelines/runs/200');
    expect(requestUrl).toContain('api-version=7.1');
  });

  it('uses build API to resolve pipeline id when not provided', async () => {
    mockBuildApi.getBuild.mockResolvedValue({ definition: { id: 123 } });
    mockRestGet.mockResolvedValue({
      statusCode: 200,
      result: baseRun,
      headers: {},
    });

    await getPipelineRun(mockConnection, {
      projectId: 'test-project',
      runId: 200,
    });

    expect(mockBuildApi.getBuild).toHaveBeenCalledWith('test-project', 200);
    const [requestUrl] = mockRestGet.mock.calls[0];
    expect(requestUrl).toContain('/pipelines/123/runs/200');
  });

  it('validates pipeline membership when provided', async () => {
    mockRestGet.mockResolvedValueOnce({
      statusCode: 200,
      result: { ...baseRun, pipeline: { id: '42' } },
      headers: {},
    });

    const run = await getPipelineRun(mockConnection, {
      projectId: 'test-project',
      runId: 200,
      pipelineId: 42,
    });

    expect(run.pipeline?.id).toBe('42');
  });

  it('throws resource not found when pipeline guard fails', async () => {
    mockRestGet.mockResolvedValue({
      statusCode: 200,
      result: { ...baseRun, pipeline: { id: 99 } },
      headers: {},
    });

    await expect(
      getPipelineRun(mockConnection, {
        projectId: 'test-project',
        runId: 200,
        pipelineId: 42,
      }),
    ).rejects.toBeInstanceOf(AzureDevOpsResourceNotFoundError);
  });

  it('throws resource not found when pipeline information is missing but guard provided', async () => {
    mockRestGet.mockResolvedValue({
      statusCode: 200,
      result: { ...baseRun, pipeline: undefined },
      headers: {},
    });

    await expect(
      getPipelineRun(mockConnection, {
        projectId: 'test-project',
        runId: 200,
        pipelineId: 42,
      }),
    ).rejects.toBeInstanceOf(AzureDevOpsResourceNotFoundError);
  });

  it('throws resource not found when API returns 404', async () => {
    mockRestGet.mockResolvedValue({
      statusCode: 404,
      result: null,
      headers: {},
    });

    await expect(
      getPipelineRun(mockConnection, {
        projectId: 'test-project',
        runId: 404,
      }),
    ).rejects.toBeInstanceOf(AzureDevOpsResourceNotFoundError);
  });

  it('falls back to generic run endpoint when pipeline-specific lookup fails', async () => {
    mockRestGet
      .mockResolvedValueOnce({ statusCode: 404, result: null, headers: {} })
      .mockResolvedValueOnce({
        statusCode: 200,
        result: baseRun,
        headers: {},
      });

    const run = await getPipelineRun(mockConnection, {
      projectId: 'test-project',
      runId: 200,
      pipelineId: 42,
    });

    expect(run).toEqual(baseRun);
    expect(mockRestGet).toHaveBeenCalledTimes(2);
    const [firstUrl] = mockRestGet.mock.calls[0];
    const [secondUrl] = mockRestGet.mock.calls[1];
    expect(firstUrl).toContain('/pipelines/42/runs/200');
    expect(secondUrl).toContain('/pipelines/runs/200');
  });

  it('maps authentication errors', async () => {
    mockRestGet.mockRejectedValue(new Error('Unauthorized 401'));

    await expect(
      getPipelineRun(mockConnection, {
        projectId: 'test-project',
        runId: 200,
      }),
    ).rejects.toBeInstanceOf(AzureDevOpsAuthenticationError);
  });

  it('wraps unexpected errors', async () => {
    mockRestGet.mockRejectedValue(new Error('Something went wrong'));

    await expect(
      getPipelineRun(mockConnection, {
        projectId: 'test-project',
        runId: 200,
      }),
    ).rejects.toBeInstanceOf(AzureDevOpsError);
  });
});

```

--------------------------------------------------------------------------------
/src/features/search/search-work-items/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,
} from '../../../shared/errors';
import {
  SearchWorkItemsOptions,
  WorkItemSearchRequest,
  WorkItemSearchResponse,
} from '../types';

/**
 * Search for work items in Azure DevOps projects
 *
 * @param connection The Azure DevOps WebApi connection
 * @param options Parameters for searching work items
 * @returns Search results with work item details and highlights
 */
export async function searchWorkItems(
  connection: WebApi,
  options: SearchWorkItemsOptions,
): Promise<WorkItemSearchResponse> {
  try {
    // Prepare the search request
    const searchRequest: WorkItemSearchRequest = {
      searchText: options.searchText,
      $skip: options.skip,
      $top: options.top,
      filters: {
        ...(options.projectId
          ? { 'System.TeamProject': [options.projectId] }
          : {}),
        ...options.filters,
      },
      includeFacets: options.includeFacets,
      $orderBy: options.orderBy,
    };

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

    // Extract organization and project from the connection URL
    const { organization, project } = extractOrgAndProject(
      connection,
      options.projectId,
    );

    // Make the search API request
    // If projectId is provided, include it in the URL, otherwise perform organization-wide search
    const searchUrl = options.projectId
      ? `https://almsearch.dev.azure.com/${organization}/${project}/_apis/search/workitemsearchresults?api-version=7.1`
      : `https://almsearch.dev.azure.com/${organization}/_apis/search/workitemsearchresults?api-version=7.1`;

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

    return searchResponse.data;
  } catch (error) {
    // If it's already an AzureDevOpsError, rethrow it
    if (error instanceof AzureDevOpsError) {
      throw error;
    }

    // Handle axios errors
    if (axios.isAxiosError(error)) {
      const status = error.response?.status;
      const message = error.response?.data?.message || error.message;

      if (status === 404) {
        throw new AzureDevOpsResourceNotFoundError(
          `Resource not found: ${message}`,
        );
      } else if (status === 400) {
        throw new AzureDevOpsValidationError(
          `Invalid request: ${message}`,
          error.response?.data,
        );
      } else if (status === 401 || status === 403) {
        throw new AzureDevOpsPermissionError(`Permission denied: ${message}`);
      } else {
        // For other axios errors, wrap in a generic AzureDevOpsError
        throw new AzureDevOpsError(`Azure DevOps API error: ${message}`);
      }
      // This code is unreachable but TypeScript doesn't know that
    }

    // Otherwise, wrap it in a generic error
    throw new AzureDevOpsError(
      `Failed to search work items: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

/**
 * Extract organization and project from the connection URL
 *
 * @param connection The Azure DevOps WebApi connection
 * @param projectId The project ID or name (optional)
 * @returns The organization and project
 */
function extractOrgAndProject(
  connection: WebApi,
  projectId?: string,
): { organization: string; project: 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,
    project: projectId || '',
  };
}

/**
 * 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)}`,
    );
  }
}

```

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

```markdown
# Azure DevOps Wiki Tools

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

## get_wikis

Lists all wikis in a project or organization.

### Description

The `get_wikis` tool retrieves all wikis available in a specified Azure DevOps project or organization. This is useful for discovering which wikis are available before working with specific wiki pages.

### Parameters

- `organizationId` (optional): The ID or name of the organization. If not provided, the default organization from environment settings will be used.
- `projectId` (optional): The ID or name of the project. If not provided, the default project from environment settings will be used.

```json
{
  "organizationId": "MyOrganization",
  "projectId": "MyProject"
}
```

### Response

The tool returns an array of wiki objects, each containing:

- `id`: The unique identifier of the wiki
- `name`: The name of the wiki
- `url`: The URL of the wiki
- Other wiki properties such as `remoteUrl` and `type`

Example response:

```json
[
  {
    "id": "wiki1-id",
    "name": "MyWiki",
    "type": "projectWiki",
    "url": "https://dev.azure.com/MyOrganization/MyProject/_wiki/wikis/MyWiki",
    "remoteUrl": "https://dev.azure.com/MyOrganization/MyProject/_git/MyWiki"
  }
]
```

## get_wiki_page

Gets the content of a specific wiki page.

### Description

The `get_wiki_page` tool retrieves the content of a specified wiki page as plain text. This is useful for viewing the content of wiki pages programmatically.

### Parameters

- `organizationId` (optional): The ID or name of the organization. If not provided, the default organization from environment settings will be used.
- `projectId` (optional): The ID or name of the project. If not provided, the default project from environment settings will be used.
- `wikiId` (required): The ID or name of the wiki containing the page.
- `pagePath` (required): The path of the page within the wiki (e.g., "/Home" or "/Folder/Page").

```json
{
  "organizationId": "MyOrganization",
  "projectId": "MyProject",
  "wikiId": "MyWiki",
  "pagePath": "/Home"
}
```

### Response

The tool returns the content of the wiki page as a string in markdown format.

Example response:

```markdown
# Welcome to the Wiki

This is the home page of the wiki.

## Getting Started

Here are some links to help you get started:
- [Documentation](/Documentation)
- [Tutorials](/Tutorials)
- [FAQ](/FAQ)
```

### Error Handling

The tool may throw the following errors:

- `AzureDevOpsResourceNotFoundError`: If the specified wiki or page does not exist
- `AzureDevOpsPermissionError`: If the authenticated user does not have permission to access the wiki
- General errors: If other unexpected errors occur during the request

### Example Usage

```typescript
// Example MCP client call
const result = await mcpClient.callTool('get_wiki_page', {
  projectId: 'MyProject',
  wikiId: 'MyWiki',
  pagePath: '/Home'
});
console.log(result);
```

### Implementation Details

This tool uses the Azure DevOps REST API to retrieve the wiki page content with the `Accept: text/plain` header to get the content directly in text format. The page path is properly encoded to handle spaces and special characters in the URL.

## list_wiki_pages

Lists all pages within a specified Azure DevOps wiki.

### Description

The `list_wiki_pages` tool retrieves a list of all pages within a specified wiki. It returns summary information for each page, including the page ID, path, URL, and order. This is useful for discovering the structure and contents of a wiki before working with specific pages.

### Parameters

- `organizationId` (optional): The ID or name of the organization. If not provided, the default organization from environment settings will be used.
- `projectId` (optional): The ID or name of the project. If not provided, the default project from environment settings will be used.
- `wikiId` (required): The ID or name of the wiki to list pages from.

```json
{
  "organizationId": "MyOrganization",
  "projectId": "MyProject",
  "wikiId": "MyWiki"
}
```

### Response

The tool returns an array of wiki page summary objects, each containing:

- `id`: The unique numeric identifier of the page
- `path`: The path of the page within the wiki (e.g., "/Home" or "/Folder/Page")
- `url`: The URL to access the page (optional)
- `order`: The display order of the page (optional)

Example response:

```json
[
  {
    "id": 1,
    "path": "/Home",
    "url": "https://dev.azure.com/MyOrganization/MyProject/_wiki/wikis/MyWiki/1/Home",
    "order": 0
  },
  {
    "id": 2,
    "path": "/Documentation",
    "url": "https://dev.azure.com/MyOrganization/MyProject/_wiki/wikis/MyWiki/2/Documentation",
    "order": 1
  },
  {
    "id": 3,
    "path": "/Documentation/Getting-Started",
    "url": "https://dev.azure.com/MyOrganization/MyProject/_wiki/wikis/MyWiki/3/Getting-Started",
    "order": 2
  }
]
```

### Error Handling

The tool may throw the following errors:

- `AzureDevOpsResourceNotFoundError`: If the specified wiki does not exist
- `AzureDevOpsPermissionError`: If the authenticated user does not have permission to access the wiki
- `AzureDevOpsError`: If other unexpected errors occur during the request

### Example Usage

```typescript
// Example MCP client call
const result = await mcpClient.callTool('list_wiki_pages', {
  projectId: 'MyProject',
  wikiId: 'MyWiki'
});
console.log(result);
```

### Implementation Details

This tool uses the Azure DevOps REST API to retrieve the list of pages within a wiki. The response is mapped to provide a consistent interface with page ID, path, URL, and order information. 
```

--------------------------------------------------------------------------------
/docs/azure-identity-authentication.md:
--------------------------------------------------------------------------------

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

This guide explains how to use Azure Identity authentication with the Azure DevOps MCP Server.

## Overview

Azure Identity authentication lets you use your existing Azure credentials to authenticate with Azure DevOps, instead of creating and managing Personal Access Tokens (PATs). This approach offers several benefits:

- **Unified authentication**: Use the same credentials for Azure and Azure DevOps
- **Enhanced security**: Support for managed identities and client certificates
- **Flexible credential types**: Multiple options for different environments
- **Automatic token management**: Handles token acquisition and renewal

## Credential Types

The Azure DevOps MCP Server supports multiple credential types through the Azure Identity SDK:

### DefaultAzureCredential

This credential type attempts multiple authentication methods in sequence until one succeeds:

1. Environment variables (EnvironmentCredential)
2. Managed Identity (ManagedIdentityCredential)
3. Azure CLI (AzureCliCredential)
4. Visual Studio Code (VisualStudioCodeCredential)
5. Azure PowerShell (AzurePowerShellCredential)

It's a great option for applications that need to work across different environments without code changes.

### AzureCliCredential

This credential type uses your Azure CLI login. It's perfect for local development when you're already using the Azure CLI.

## Configuration

### Environment Variables

To use Azure Identity authentication, set the following environment variables:

```bash
# Required
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
AZURE_DEVOPS_AUTH_METHOD=azure-identity

# Optional
AZURE_DEVOPS_DEFAULT_PROJECT=your-project-name
```

For service principal authentication, add these environment variables:

```bash
AZURE_TENANT_ID=your-tenant-id
AZURE_CLIENT_ID=your-client-id
AZURE_CLIENT_SECRET=your-client-secret
```

### Use with Claude Desktop/Cursor AI

Add the following to your configuration file:

```json
{
  "mcpServers": {
    "azureDevOps": {
      "command": "npx",
      "args": ["-y", "@tiberriver256/mcp-server-azure-devops"],
      "env": {
        "AZURE_DEVOPS_ORG_URL": "https://dev.azure.com/your-organization",
        "AZURE_DEVOPS_AUTH_METHOD": "azure-identity",
        "AZURE_DEVOPS_DEFAULT_PROJECT": "your-project-name"
      }
    }
  }
}
```

## Authentication Methods

### Method 1: Using Azure CLI

1. Install the Azure CLI from [here](https://docs.microsoft.com/cli/azure/install-azure-cli)
2. Log in to Azure:
   ```bash
   az login
   ```
3. Set up your environment variables:
   ```bash
   AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
   AZURE_DEVOPS_AUTH_METHOD=azure-identity
   ```

### Method 2: Using Service Principal

1. Create a service principal in Azure AD:
   ```bash
   az ad sp create-for-rbac --name "MyAzureDevOpsApp"
   ```
2. Grant the service principal access to your Azure DevOps organization
3. Set up your environment variables:
   ```bash
   AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
   AZURE_DEVOPS_AUTH_METHOD=azure-identity
   AZURE_TENANT_ID=your-tenant-id
   AZURE_CLIENT_ID=your-client-id
   AZURE_CLIENT_SECRET=your-client-secret
   ```

### Method 3: Using Managed Identity (for Azure-hosted applications)

1. Enable managed identity for your Azure resource (VM, App Service, etc.)
2. Grant the managed identity access to your Azure DevOps organization
3. Set up your environment variables:
   ```bash
   AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
   AZURE_DEVOPS_AUTH_METHOD=azure-identity
   ```

## Troubleshooting

### Common Issues

#### Failed to acquire token

```
Error: Failed to authenticate with Azure Identity: CredentialUnavailableError: DefaultAzureCredential failed to retrieve a token
```

**Possible solutions:**
- Ensure you're logged in with `az login`
- Check if your managed identity is correctly configured
- Verify that service principal credentials are correct

#### Permission issues

```
Error: Failed to authenticate with Azure Identity: AuthorizationFailed: The client does not have authorization to perform action
```

**Possible solutions:**
- Ensure your identity has the necessary permissions in Azure DevOps
- Check if you need to add your identity to specific Azure DevOps project(s)

#### Network issues

```
Error: Failed to authenticate with Azure Identity: ClientAuthError: Interaction required
```

**Possible solutions:**
- Check your network connectivity
- Verify that your firewall allows connections to Azure services

## Best Practices

1. **Choose the right credential type for your environment**:
   - For local development: Azure CLI credential
   - For CI/CD pipelines: Service principal
   - For Azure-hosted applications: Managed identity

2. **Follow the principle of least privilege**:
   - Only grant the permissions needed for your use case
   - Regularly audit and review permissions

3. **Rotate credentials regularly**:
   - For service principals, rotate client secrets periodically
   - Use certificate-based authentication when possible for enhanced security

## Examples

### Basic configuration with Azure CLI

```bash
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/mycompany
AZURE_DEVOPS_AUTH_METHOD=azure-identity
AZURE_DEVOPS_DEFAULT_PROJECT=MyProject
```

### Service principal authentication

```bash
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/mycompany
AZURE_DEVOPS_AUTH_METHOD=azure-identity
AZURE_DEVOPS_DEFAULT_PROJECT=MyProject
AZURE_TENANT_ID=00000000-0000-0000-0000-000000000000
AZURE_CLIENT_ID=11111111-1111-1111-1111-111111111111
AZURE_CLIENT_SECRET=your-client-secret
```

```

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

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

describe('addPullRequestComment integration', () => {
  let connection: WebApi | null = null;
  let projectName: string;
  let repositoryName: string;
  let pullRequestId: 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`);
    } catch (error) {
      console.error('Error in test setup:', error);
      throw error;
    }
  });

  test('should add a new comment thread to pull request', 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 commentContent = `Test comment ${timestamp}-${randomSuffix}`;

    const result = await addPullRequestComment(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        content: commentContent,
        status: 'active',
      },
    );

    // Verify the comment was created
    expect(result.comment).toBeDefined();
    expect(result.comment.content).toBe(commentContent);
    expect(result.thread).toBeDefined();
    expect(result.thread!.status).toBe('active'); // Transformed to string
  }, 30000); // 30 second timeout for integration test

  test('should add a file comment to pull request', 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 commentContent = `File comment ${timestamp}-${randomSuffix}`;
    const filePath = '/README.md'; // Assuming README.md exists in the repo
    const lineNumber = 1;

    const result = await addPullRequestComment(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        content: commentContent,
        filePath,
        lineNumber,
        status: 'active',
      },
    );

    // Verify the file comment was created
    expect(result.comment).toBeDefined();
    expect(result.comment.content).toBe(commentContent);
    expect(result.thread).toBeDefined();
    expect(result.thread!.threadContext).toBeDefined();
    expect(result.thread!.threadContext!.filePath).toBe(filePath);
    expect(result.thread!.threadContext!.rightFileStart!.line).toBe(lineNumber);
  }, 30000); // 30 second timeout for integration test

  test('should add a reply to an existing comment thread', 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;
    }

    // First create a thread
    const initialComment = await addPullRequestComment(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        content: `Initial comment ${timestamp}-${randomSuffix}`,
        status: 'active',
      },
    );

    const threadId = initialComment.thread!.id!;
    const replyContent = `Reply comment ${timestamp}-${randomSuffix}`;

    // Add a reply to the thread
    const result = await addPullRequestComment(
      connection,
      projectName,
      repositoryName,
      pullRequestId,
      {
        projectId: projectName,
        repositoryId: repositoryName,
        pullRequestId,
        content: replyContent,
        threadId,
      },
    );

    // Verify the reply was created
    expect(result.comment).toBeDefined();
    expect(result.comment.content).toBe(replyContent);
    expect(result.thread).toBeUndefined(); // No thread returned for replies
  }, 30000); // 30 second timeout for integration test
});

```

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

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

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

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

  test('should create a new work item in 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',
      );
    }

    // Create a unique title using timestamp to avoid conflicts
    const uniqueTitle = `Test Work Item ${new Date().toISOString()}`;

    // For a true integration test, use a real project
    const projectName =
      process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
    const workItemType = 'Task'; // Assumes 'Task' type exists in the project

    const options: CreateWorkItemOptions = {
      title: uniqueTitle,
      description: 'This is a test work item created by an integration test',
      priority: 2,
    };

    // Act - make an actual API call to Azure DevOps
    const result = await createWorkItem(
      connection,
      projectName,
      workItemType,
      options,
    );

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.id).toBeDefined();

    // Verify fields match what we set
    expect(result.fields).toBeDefined();
    if (result.fields) {
      expect(result.fields['System.Title']).toBe(uniqueTitle);
      expect(result.fields['Microsoft.VSTS.Common.Priority']).toBe(2);
    }
  });

  test('should create a work item with additional fields', 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',
      );
    }

    // Create a unique title using timestamp to avoid conflicts
    const uniqueTitle = `Test Work Item with Fields ${new Date().toISOString()}`;

    // For a true integration test, use a real project
    const projectName =
      process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
    const workItemType = 'Task';

    const options: CreateWorkItemOptions = {
      title: uniqueTitle,
      description: 'This is a test work item with additional fields',
      priority: 1,
      additionalFields: {
        'System.Tags': 'Integration Test,Automated',
      },
    };

    // Act - make an actual API call to Azure DevOps
    const result = await createWorkItem(
      connection,
      projectName,
      workItemType,
      options,
    );

    // Assert on the actual response
    expect(result).toBeDefined();
    expect(result.id).toBeDefined();

    // Verify fields match what we set
    expect(result.fields).toBeDefined();
    if (result.fields) {
      expect(result.fields['System.Title']).toBe(uniqueTitle);
      expect(result.fields['Microsoft.VSTS.Common.Priority']).toBe(1);
      // Just check that tags contain both values, order may vary
      expect(result.fields['System.Tags']).toContain('Integration Test');
      expect(result.fields['System.Tags']).toContain('Automated');
    }
  });

  test('should create a child work item with parent-child relationship', 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',
      );
    }

    // For a true integration test, use a real project
    const projectName =
      process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';

    // First, create a parent work item (User Story)
    const parentTitle = `Parent Story ${new Date().toISOString()}`;
    const parentOptions: CreateWorkItemOptions = {
      title: parentTitle,
      description: 'This is a parent user story',
    };

    const parentResult = await createWorkItem(
      connection,
      projectName,
      'User Story', // Assuming User Story type exists
      parentOptions,
    );

    expect(parentResult).toBeDefined();
    expect(parentResult.id).toBeDefined();
    const parentId = parentResult.id;

    // Now create a child work item (Task) with a link to the parent
    const childTitle = `Child Task ${new Date().toISOString()}`;
    const childOptions: CreateWorkItemOptions = {
      title: childTitle,
      description: 'This is a child task of a user story',
      parentId: parentId, // Reference to parent work item
    };

    const childResult = await createWorkItem(
      connection,
      projectName,
      'Task',
      childOptions,
    );

    // Assert the child work item was created
    expect(childResult).toBeDefined();
    expect(childResult.id).toBeDefined();

    // Now verify the parent-child relationship
    // We would need to fetch the relations, but for now we'll just assert
    // that the response indicates a relationship was created
    expect(childResult.relations).toBeDefined();

    // Check that at least one relation exists that points to our parent
    const parentRelation = childResult.relations?.find(
      (relation) =>
        relation.rel === 'System.LinkTypes.Hierarchy-Reverse' &&
        relation.url &&
        relation.url.includes(`/${parentId}`),
    );
    expect(parentRelation).toBeDefined();
  });
});

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import {
  AzureDevOpsError,
  AzureDevOpsResourceNotFoundError,
  AzureDevOpsValidationError,
  AzureDevOpsPermissionError,
} from '../../../shared/errors';
import { createWiki } from './feature';
import { WikiType } from './schema';
import { getWikiClient } from '../../../clients/azure-devops';

// Mock the WikiClient
jest.mock('../../../clients/azure-devops');

describe('createWiki unit', () => {
  // Mock WikiClient
  const mockWikiClient = {
    createWiki: jest.fn(),
  };

  // Mock WebApi connection (kept for backward compatibility)
  const mockConnection = {} as WebApi;

  beforeEach(() => {
    // Clear mock calls between tests
    jest.clearAllMocks();
    // Setup mock response for getWikiClient
    (getWikiClient as jest.Mock).mockResolvedValue(mockWikiClient);
  });

  test('should create a project wiki', async () => {
    // Mock data
    const mockWiki = {
      id: 'wiki1',
      name: 'Project Wiki',
      projectId: 'project1',
      remoteUrl: 'https://example.com/wiki1',
      url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1',
      type: 'projectWiki',
      repositoryId: 'repo1',
      mappedPath: '/',
    };

    // Setup mock response
    mockWikiClient.createWiki.mockResolvedValue(mockWiki);

    // Call the function
    const result = await createWiki(mockConnection, {
      name: 'Project Wiki',
      projectId: 'project1',
    });

    // Assertions
    expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
    expect(mockWikiClient.createWiki).toHaveBeenCalledWith('project1', {
      name: 'Project Wiki',
      projectId: 'project1',
      type: WikiType.ProjectWiki,
    });
    expect(result).toEqual(mockWiki);
  });

  test('should create a code wiki', async () => {
    // Mock data
    const mockWiki = {
      id: 'wiki2',
      name: 'Code Wiki',
      projectId: 'project1',
      repositoryId: 'repo1',
      mappedPath: '/docs',
      remoteUrl: 'https://example.com/wiki2',
      url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki2',
      type: 'codeWiki',
    };

    // Setup mock response
    mockWikiClient.createWiki.mockResolvedValue(mockWiki);

    // Call the function
    const result = await createWiki(mockConnection, {
      name: 'Code Wiki',
      projectId: 'project1',
      type: WikiType.CodeWiki,
      repositoryId: 'repo1',
      mappedPath: '/docs',
    });

    // Assertions
    expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
    expect(mockWikiClient.createWiki).toHaveBeenCalledWith('project1', {
      name: 'Code Wiki',
      projectId: 'project1',
      type: WikiType.CodeWiki,
      repositoryId: 'repo1',
      mappedPath: '/docs',
      version: {
        version: 'main',
        versionType: 'branch' as const,
      },
    });
    expect(result).toEqual(mockWiki);
  });

  test('should throw validation error when repository ID is missing for code wiki', async () => {
    // Call the function and expect it to throw
    await expect(
      createWiki(mockConnection, {
        name: 'Code Wiki',
        projectId: 'project1',
        type: WikiType.CodeWiki,
        // repositoryId is missing
      }),
    ).rejects.toThrow(AzureDevOpsValidationError);

    // Assertions
    expect(getWikiClient).not.toHaveBeenCalled();
    expect(mockWikiClient.createWiki).not.toHaveBeenCalled();
  });

  test('should handle project not found error', async () => {
    // Setup mock to throw an error
    mockWikiClient.createWiki.mockRejectedValue(
      new AzureDevOpsResourceNotFoundError('Project not found'),
    );

    // Call the function and expect it to throw
    await expect(
      createWiki(mockConnection, {
        name: 'Project Wiki',
        projectId: 'nonExistentProject',
      }),
    ).rejects.toThrow(AzureDevOpsResourceNotFoundError);

    // Assertions
    expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
    expect(mockWikiClient.createWiki).toHaveBeenCalled();
  });

  test('should handle repository not found error', async () => {
    // Setup mock to throw an error
    mockWikiClient.createWiki.mockRejectedValue(
      new AzureDevOpsResourceNotFoundError('Repository not found'),
    );

    // Call the function and expect it to throw
    await expect(
      createWiki(mockConnection, {
        name: 'Code Wiki',
        projectId: 'project1',
        type: WikiType.CodeWiki,
        repositoryId: 'nonExistentRepo',
      }),
    ).rejects.toThrow(AzureDevOpsResourceNotFoundError);

    // Assertions
    expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
    expect(mockWikiClient.createWiki).toHaveBeenCalled();
  });

  test('should handle permission error', async () => {
    // Setup mock to throw an error
    mockWikiClient.createWiki.mockRejectedValue(
      new AzureDevOpsPermissionError('You do not have permission'),
    );

    // Call the function and expect it to throw
    await expect(
      createWiki(mockConnection, {
        name: 'Project Wiki',
        projectId: 'project1',
      }),
    ).rejects.toThrow(AzureDevOpsPermissionError);

    // Assertions
    expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
    expect(mockWikiClient.createWiki).toHaveBeenCalled();
  });

  test('should handle generic errors', async () => {
    // Setup mock to throw an error
    mockWikiClient.createWiki.mockRejectedValue(new Error('Unknown error'));

    // Call the function and expect it to throw
    await expect(
      createWiki(mockConnection, {
        name: 'Project Wiki',
        projectId: 'project1',
      }),
    ).rejects.toThrow(AzureDevOpsError);

    // Assertions
    expect(getWikiClient).toHaveBeenCalledWith({ organizationId: undefined });
    expect(mockWikiClient.createWiki).toHaveBeenCalled();
  });
});

```

--------------------------------------------------------------------------------
/src/features/repositories/create-commit/feature.ts:
--------------------------------------------------------------------------------

```typescript
import { WebApi } from 'azure-devops-node-api';
import {
  GitVersionType,
  GitRefUpdate,
  GitChange,
  VersionControlChangeType,
  ItemContentType,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { applyPatch, parsePatch, createTwoFilesPatch } from 'diff';
import { AzureDevOpsError } from '../../../shared/errors';
import { CreateCommitOptions } from '../types';

async function streamToString(stream: NodeJS.ReadableStream): Promise<string> {
  const chunks: Buffer[] = [];
  return await new Promise<string>((resolve, reject) => {
    stream.on('data', (c) => chunks.push(Buffer.from(c)));
    stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
    stream.on('error', (err) => reject(err));
  });
}

/**
 * Create a commit with multiple file changes
 */
export async function createCommit(
  connection: WebApi,
  options: CreateCommitOptions,
): Promise<void> {
  try {
    const gitApi = await connection.getGitApi();
    const branch = await gitApi.getBranch(
      options.repositoryId,
      options.branchName,
      options.projectId,
    );
    const baseCommit = branch?.commit?.commitId;
    if (!baseCommit) {
      throw new AzureDevOpsError(`Branch '${options.branchName}' not found`);
    }

    const changes: GitChange[] = [];

    for (const file of options.changes) {
      // Handle search/replace format by generating a patch
      let patchString = file.patch;

      if (
        !patchString &&
        file.search !== undefined &&
        file.replace !== undefined
      ) {
        if (!file.path) {
          throw new AzureDevOpsError(
            'path is required when using search/replace format',
          );
        }

        // Fetch current file content
        let currentContent = '';
        try {
          const stream = await gitApi.getItemContent(
            options.repositoryId,
            file.path,
            options.projectId,
            undefined,
            undefined,
            undefined,
            undefined,
            false,
            { version: options.branchName, versionType: GitVersionType.Branch },
            true,
          );
          currentContent = stream ? await streamToString(stream) : '';
        } catch {
          // File might not exist (new file scenario) - treat as empty
          currentContent = '';
        }

        // Perform the replacement
        if (!currentContent.includes(file.search)) {
          throw new AzureDevOpsError(
            `Search string not found in ${file.path}. The file may have been modified since you last read it.`,
          );
        }

        const newContent = currentContent.replace(file.search, file.replace);

        // Generate proper unified diff
        patchString = createTwoFilesPatch(
          file.path,
          file.path,
          currentContent,
          newContent,
          undefined,
          undefined,
        );
      }

      if (!patchString) {
        throw new AzureDevOpsError(
          'Either patch or both search and replace must be provided for each change',
        );
      }

      const patches = parsePatch(patchString);
      if (patches.length !== 1) {
        throw new AzureDevOpsError(
          `Expected a single file diff for change but received ${patches.length}`,
        );
      }

      const patch = patches[0];

      const normalizePath = (path?: string | null): string | undefined => {
        if (!path || path === '/dev/null') {
          return undefined;
        }
        return path.replace(/^a\//, '').replace(/^b\//, '');
      };

      const oldPath = normalizePath(patch.oldFileName);
      const newPath = normalizePath(patch.newFileName);
      const targetPath = file.path ?? newPath ?? oldPath;

      if (!targetPath) {
        throw new AzureDevOpsError(
          'Unable to determine target path for change',
        );
      }

      if (oldPath && newPath && oldPath !== newPath) {
        throw new AzureDevOpsError(
          `Renaming files is not supported (attempted ${oldPath} -> ${newPath})`,
        );
      }

      let originalContent = '';

      if (oldPath) {
        const stream = await gitApi.getItemContent(
          options.repositoryId,
          oldPath,
          options.projectId,
          undefined,
          undefined,
          undefined,
          undefined,
          false,
          { version: options.branchName, versionType: GitVersionType.Branch },
          true,
        );
        originalContent = stream ? await streamToString(stream) : '';
      }

      const patchedContent = applyPatch(originalContent, patch);

      if (patchedContent === false) {
        throw new AzureDevOpsError(
          `Failed to apply diff for ${targetPath}. Please ensure the patch is up to date with the branch head.`,
        );
      }

      if (!newPath) {
        changes.push({
          changeType: VersionControlChangeType.Delete,
          item: { path: targetPath },
        });
        continue;
      }

      const changeType = oldPath
        ? VersionControlChangeType.Edit
        : VersionControlChangeType.Add;

      changes.push({
        changeType,
        item: { path: targetPath },
        newContent: {
          content: patchedContent,
          contentType: ItemContentType.RawText,
        },
      });
    }

    const commit = {
      comment: options.commitMessage,
      changes,
    };

    const refUpdate: GitRefUpdate = {
      name: `refs/heads/${options.branchName}`,
      oldObjectId: baseCommit,
    };

    await gitApi.createPush(
      { commits: [commit], refUpdates: [refUpdate] },
      options.repositoryId,
      options.projectId,
    );
  } catch (error) {
    if (error instanceof AzureDevOpsError) {
      throw error;
    }
    throw new Error(
      `Failed to create commit: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

```

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

```typescript
export * from './schemas';
export * from './types';
export * from './create-pull-request';
export * from './list-pull-requests';
export * from './get-pull-request-comments';
export * from './add-pull-request-comment';
export * from './update-pull-request';
export * from './get-pull-request-changes';
export * from './get-pull-request-checks';

// 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 {
  RequestIdentifier,
  RequestHandler,
} from '../../shared/types/request-handler';
import { defaultProject } from '../../utils/environment';
import {
  CreatePullRequestSchema,
  ListPullRequestsSchema,
  GetPullRequestCommentsSchema,
  AddPullRequestCommentSchema,
  UpdatePullRequestSchema,
  GetPullRequestChangesSchema,
  GetPullRequestChecksSchema,
  createPullRequest,
  listPullRequests,
  getPullRequestComments,
  addPullRequestComment,
  updatePullRequest,
  getPullRequestChanges,
  getPullRequestChecks,
} from './';

/**
 * Checks if the request is for the pull requests feature
 */
export const isPullRequestsRequest: RequestIdentifier = (
  request: CallToolRequest,
): boolean => {
  const toolName = request.params.name;
  return [
    'create_pull_request',
    'list_pull_requests',
    'get_pull_request_comments',
    'add_pull_request_comment',
    'update_pull_request',
    'get_pull_request_changes',
    'get_pull_request_checks',
  ].includes(toolName);
};

/**
 * Handles pull requests feature requests
 */
export const handlePullRequestsRequest: RequestHandler = async (
  connection: WebApi,
  request: CallToolRequest,
): Promise<{ content: Array<{ type: string; text: string }> }> => {
  switch (request.params.name) {
    case 'create_pull_request': {
      const args = CreatePullRequestSchema.parse(request.params.arguments);
      const result = await createPullRequest(
        connection,
        args.projectId ?? defaultProject,
        args.repositoryId,
        args,
      );
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'list_pull_requests': {
      const params = ListPullRequestsSchema.parse(request.params.arguments);
      const result = await listPullRequests(
        connection,
        params.projectId ?? defaultProject,
        params.repositoryId,
        {
          projectId: params.projectId ?? defaultProject,
          repositoryId: params.repositoryId,
          status: params.status,
          creatorId: params.creatorId,
          reviewerId: params.reviewerId,
          sourceRefName: params.sourceRefName,
          targetRefName: params.targetRefName,
          top: params.top,
          skip: params.skip,
          pullRequestId: params.pullRequestId,
        },
      );
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'get_pull_request_comments': {
      const params = GetPullRequestCommentsSchema.parse(
        request.params.arguments,
      );
      const result = await getPullRequestComments(
        connection,
        params.projectId ?? defaultProject,
        params.repositoryId,
        params.pullRequestId,
        {
          projectId: params.projectId ?? defaultProject,
          repositoryId: params.repositoryId,
          pullRequestId: params.pullRequestId,
          threadId: params.threadId,
          includeDeleted: params.includeDeleted,
          top: params.top,
        },
      );
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'add_pull_request_comment': {
      const params = AddPullRequestCommentSchema.parse(
        request.params.arguments,
      );
      const result = await addPullRequestComment(
        connection,
        params.projectId ?? defaultProject,
        params.repositoryId,
        params.pullRequestId,
        {
          projectId: params.projectId ?? defaultProject,
          repositoryId: params.repositoryId,
          pullRequestId: params.pullRequestId,
          content: params.content,
          threadId: params.threadId,
          parentCommentId: params.parentCommentId,
          filePath: params.filePath,
          lineNumber: params.lineNumber,
          status: params.status,
        },
      );
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'update_pull_request': {
      const params = UpdatePullRequestSchema.parse(request.params.arguments);
      const fixedParams = {
        ...params,
        projectId: params.projectId ?? defaultProject,
      };
      const result = await updatePullRequest(fixedParams);
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'get_pull_request_changes': {
      const params = GetPullRequestChangesSchema.parse(
        request.params.arguments,
      );
      const result = await getPullRequestChanges(connection, {
        projectId: params.projectId ?? defaultProject,
        repositoryId: params.repositoryId,
        pullRequestId: params.pullRequestId,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    case 'get_pull_request_checks': {
      const params = GetPullRequestChecksSchema.parse(request.params.arguments);
      const result = await getPullRequestChecks(connection, {
        projectId: params.projectId ?? defaultProject,
        repositoryId: params.repositoryId,
        pullRequestId: params.pullRequestId,
      });
      return {
        content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
      };
    }
    default:
      throw new Error(`Unknown pull requests tool: ${request.params.name}`);
  }
};

```

--------------------------------------------------------------------------------
/src/features/search/search-wiki/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,
} from '../../../shared/errors';
import {
  SearchWikiOptions,
  WikiSearchRequest,
  WikiSearchResponse,
} from '../types';

/**
 * Search for wiki pages in Azure DevOps projects
 *
 * @param connection The Azure DevOps WebApi connection
 * @param options Parameters for searching wiki pages
 * @returns Search results for wiki pages
 */
export async function searchWiki(
  connection: WebApi,
  options: SearchWikiOptions,
): Promise<WikiSearchResponse> {
  try {
    // Prepare the search request
    const searchRequest: WikiSearchRequest = {
      searchText: options.searchText,
      $skip: options.skip,
      $top: options.top,
      filters: options.projectId
        ? {
            Project: [options.projectId],
          }
        : {},
      includeFacets: options.includeFacets,
    };

    // Add custom filters if provided
    if (
      options.filters &&
      options.filters.Project &&
      options.filters.Project.length > 0
    ) {
      if (!searchRequest.filters) {
        searchRequest.filters = {};
      }

      if (!searchRequest.filters.Project) {
        searchRequest.filters.Project = [];
      }

      searchRequest.filters.Project = [
        ...(searchRequest.filters.Project || []),
        ...options.filters.Project,
      ];
    }

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

    // Extract organization and project from the connection URL
    const { organization, project } = extractOrgAndProject(
      connection,
      options.projectId,
    );

    // Make the search API request
    // If projectId is provided, include it in the URL, otherwise perform organization-wide search
    const searchUrl = options.projectId
      ? `https://almsearch.dev.azure.com/${organization}/${project}/_apis/search/wikisearchresults?api-version=7.1`
      : `https://almsearch.dev.azure.com/${organization}/_apis/search/wikisearchresults?api-version=7.1`;

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

    return searchResponse.data;
  } catch (error) {
    // If it's already an AzureDevOpsError, rethrow it
    if (error instanceof AzureDevOpsError) {
      throw error;
    }

    // Handle axios errors
    if (axios.isAxiosError(error)) {
      const status = error.response?.status;
      const message = error.response?.data?.message || error.message;

      if (status === 404) {
        throw new AzureDevOpsResourceNotFoundError(
          `Resource not found: ${message}`,
        );
      } else if (status === 400) {
        throw new AzureDevOpsValidationError(
          `Invalid request: ${message}`,
          error.response?.data,
        );
      } else if (status === 401 || status === 403) {
        throw new AzureDevOpsPermissionError(`Permission denied: ${message}`);
      } else {
        // For other axios errors, wrap in a generic AzureDevOpsError
        throw new AzureDevOpsError(`Azure DevOps API error: ${message}`);
      }

      // This return is never reached but helps TypeScript understand the control flow
      return null as never;
    }

    // Otherwise, wrap it in a generic error
    throw new AzureDevOpsError(
      `Failed to search wiki: ${error instanceof Error ? error.message : String(error)}`,
    );
  }
}

/**
 * Extract organization and project from the connection URL
 *
 * @param connection The Azure DevOps WebApi connection
 * @param projectId The project ID or name (optional)
 * @returns The organization and project
 */
function extractOrgAndProject(
  connection: WebApi,
  projectId?: string,
): { organization: string; project: 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,
    project: projectId || '',
  };
}

/**
 * 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)}`,
    );
  }
}

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { PullRequest } from '../types';
import { listPullRequests } from './feature';
import { createPullRequest } from '../create-pull-request/feature';

import {
  getTestConnection,
  shouldSkipIntegrationTest,
} from '../../../shared/test/test-helpers';

describe('listPullRequests integration', () => {
  let connection: WebApi | null = null;
  let testPullRequest: PullRequest | null = null;
  let projectName: string;
  let repositoryName: string;

  // Generate unique branch name and PR title using timestamp
  const timestamp = Date.now();
  const randomSuffix = Math.floor(Math.random() * 1000);
  const uniqueBranchName = `test-branch-${timestamp}-${randomSuffix}`;
  const uniqueTitle = `Test PR ${timestamp}-${randomSuffix}`;

  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;
    }
  });

  afterAll(async () => {
    // Clean up created resources if needed
    if (
      testPullRequest &&
      testPullRequest.pullRequestId &&
      !shouldSkipIntegrationTest()
    ) {
      try {
        // Abandon the test pull request if it was created
        const gitApi = await connection?.getGitApi();
        if (gitApi) {
          await gitApi.updatePullRequest(
            {
              status: 2, // 2 = Abandoned
            },
            repositoryName,
            testPullRequest.pullRequestId,
            projectName,
          );
        }
      } catch (error) {
        console.error('Error cleaning up test pull request:', error);
      }
    }
  });

  test('should list pull requests from repository', 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;
    }

    try {
      // Create a branch for testing
      const gitApi = await connection.getGitApi();

      // Get the default branch info
      const repository = await gitApi.getRepository(
        repositoryName,
        projectName,
      );

      if (!repository || !repository.defaultBranch) {
        throw new Error('Cannot find repository or default branch');
      }

      // Get the commit to branch from
      const commits = await gitApi.getCommits(
        repositoryName,
        {
          itemVersion: {
            versionType: 0, // commit
            version: repository.defaultBranch.replace('refs/heads/', ''),
          },
          $top: 1,
        },
        projectName,
      );

      if (!commits || commits.length === 0) {
        throw new Error('Cannot find commits in repository');
      }

      // Create a new branch
      const refUpdate = {
        name: `refs/heads/${uniqueBranchName}`,
        oldObjectId: '0000000000000000000000000000000000000000',
        newObjectId: commits[0].commitId,
      };

      const updateResult = await gitApi.updateRefs(
        [refUpdate],
        repositoryName,
        projectName,
      );

      if (
        !updateResult ||
        updateResult.length === 0 ||
        !updateResult[0].success
      ) {
        throw new Error('Failed to create new branch');
      }

      // Create a test pull request
      testPullRequest = await createPullRequest(
        connection,
        projectName,
        repositoryName,
        {
          title: uniqueTitle,
          description: 'Test pull request for integration testing',
          sourceRefName: `refs/heads/${uniqueBranchName}`,
          targetRefName: repository.defaultBranch,
          isDraft: true,
        },
      );

      // List pull requests
      const pullRequests = await listPullRequests(
        connection,
        projectName,
        repositoryName,
        { projectId: projectName, repositoryId: repositoryName },
      );

      // Verify
      expect(pullRequests).toBeDefined();
      expect(pullRequests.value).toBeDefined();
      expect(Array.isArray(pullRequests.value)).toBe(true);
      expect(typeof pullRequests.count).toBe('number');
      expect(typeof pullRequests.hasMoreResults).toBe('boolean');

      // Find our test PR in the list
      const foundPR = pullRequests.value.find(
        (pr) => pr.pullRequestId === testPullRequest?.pullRequestId,
      );
      expect(foundPR).toBeDefined();
      expect(foundPR?.title).toBe(uniqueTitle);

      // Test with filters
      const filteredPRs = await listPullRequests(
        connection,
        projectName,
        repositoryName,
        {
          projectId: projectName,
          repositoryId: repositoryName,
          status: 'active',
          top: 5,
        },
      );

      expect(filteredPRs).toBeDefined();
      expect(filteredPRs.value).toBeDefined();
      expect(Array.isArray(filteredPRs.value)).toBe(true);
      expect(filteredPRs.count).toBeGreaterThanOrEqual(0);

      if (testPullRequest?.pullRequestId) {
        const singlePR = await listPullRequests(
          connection,
          projectName,
          repositoryName,
          {
            projectId: projectName,
            repositoryId: repositoryName,
            pullRequestId: testPullRequest.pullRequestId,
          },
        );

        expect(singlePR.count).toBe(1);
        expect(singlePR.value[0]?.pullRequestId).toBe(
          testPullRequest.pullRequestId,
        );
        expect(singlePR.hasMoreResults).toBe(false);
      }
    } catch (error) {
      console.error('Test error:', error);
      throw error;
    }
  }, 30000); // 30 second timeout for integration test
});

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { IGitApi } from 'azure-devops-node-api/GitApi';
import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { AzureDevOpsResourceNotFoundError } from '../../../shared/errors';
import { getFileContent } from './feature';
import { Readable } from 'stream';

describe('getFileContent', () => {
  let mockConnection: WebApi;
  let mockGitApi: IGitApi;
  const mockRepositoryId = 'test-repo';
  const mockProjectId = 'test-project';
  const mockFilePath = '/path/to/file.txt';
  const mockFileContent = 'Test file content';
  const mockItem = {
    objectId: '123456',
    path: mockFilePath,
    url: 'https://dev.azure.com/org/project/_apis/git/repositories/repo/items/path/to/file.txt',
    gitObjectType: 'blob',
  };

  // Helper function to create a readable stream from a string
  function createReadableStream(content: string): Readable {
    const stream = new Readable();
    stream.push(content);
    stream.push(null); // Signals the end of the stream
    return stream;
  }

  beforeEach(() => {
    mockGitApi = {
      getItemContent: jest
        .fn()
        .mockResolvedValue(createReadableStream(mockFileContent)),
      getItems: jest.fn().mockResolvedValue([mockItem]),
    } as unknown as IGitApi;

    mockConnection = {
      getGitApi: jest.fn().mockResolvedValue(mockGitApi),
    } as unknown as WebApi;
  });

  it('should get file content for a file in the default branch', async () => {
    const result = await getFileContent(
      mockConnection,
      mockProjectId,
      mockRepositoryId,
      mockFilePath,
    );

    expect(mockConnection.getGitApi).toHaveBeenCalled();
    expect(mockGitApi.getItems).toHaveBeenCalledWith(
      mockRepositoryId,
      mockProjectId,
      mockFilePath,
      expect.any(Number), // VersionControlRecursionType.OneLevel
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
    );

    expect(mockGitApi.getItemContent).toHaveBeenCalledWith(
      mockRepositoryId,
      mockFilePath,
      mockProjectId,
      undefined,
      undefined,
      undefined,
      undefined,
      false,
      undefined,
      true,
    );

    expect(result).toEqual({
      content: mockFileContent,
      isDirectory: false,
    });
  });

  it('should get file content for a file in a specific branch', async () => {
    const branchName = 'test-branch';
    const versionDescriptor = {
      versionType: GitVersionType.Branch,
      version: branchName,
      versionOptions: undefined,
    };

    const result = await getFileContent(
      mockConnection,
      mockProjectId,
      mockRepositoryId,
      mockFilePath,
      {
        versionType: GitVersionType.Branch,
        version: branchName,
      },
    );

    expect(mockConnection.getGitApi).toHaveBeenCalled();
    expect(mockGitApi.getItems).toHaveBeenCalledWith(
      mockRepositoryId,
      mockProjectId,
      mockFilePath,
      expect.any(Number), // VersionControlRecursionType.OneLevel
      undefined,
      undefined,
      undefined,
      undefined,
      versionDescriptor,
    );

    expect(mockGitApi.getItemContent).toHaveBeenCalledWith(
      mockRepositoryId,
      mockFilePath,
      mockProjectId,
      undefined,
      undefined,
      undefined,
      undefined,
      false,
      versionDescriptor,
      true,
    );

    expect(result).toEqual({
      content: mockFileContent,
      isDirectory: false,
    });
  });

  it('should throw an error if the file is not found', async () => {
    // Mock getItems to throw an error
    mockGitApi.getItems = jest
      .fn()
      .mockRejectedValue(new Error('Item not found'));

    // Mock getItemContent to throw a specific error indicating not found
    mockGitApi.getItemContent = jest
      .fn()
      .mockRejectedValue(new Error('Item not found'));

    await expect(
      getFileContent(
        mockConnection,
        mockProjectId,
        mockRepositoryId,
        '/invalid/path',
      ),
    ).rejects.toThrow(AzureDevOpsResourceNotFoundError);
  });

  it('should get directory content if the path is a directory', async () => {
    const dirPath = '/path/to/dir';
    const mockDirectoryItems = [
      {
        path: `${dirPath}/file1.txt`,
        gitObjectType: 'blob',
        isFolder: false,
      },
      {
        path: `${dirPath}/file2.md`,
        gitObjectType: 'blob',
        isFolder: false,
      },
      {
        path: `${dirPath}/subdir`,
        gitObjectType: 'tree',
        isFolder: true,
      },
    ];

    // Mock getItems to return multiple items, indicating a directory
    mockGitApi.getItems = jest.fn().mockResolvedValue(mockDirectoryItems);

    const result = await getFileContent(
      mockConnection,
      mockProjectId,
      mockRepositoryId,
      dirPath,
    );

    expect(mockConnection.getGitApi).toHaveBeenCalled();
    expect(mockGitApi.getItems).toHaveBeenCalledWith(
      mockRepositoryId,
      mockProjectId,
      dirPath,
      expect.any(Number), // VersionControlRecursionType.OneLevel
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
    );

    // Should not attempt to get file content for a directory
    expect(mockGitApi.getItemContent).not.toHaveBeenCalled();

    expect(result).toEqual({
      content: JSON.stringify(mockDirectoryItems, null, 2),
      isDirectory: true,
    });
  });

  it('should handle a directory path with trailing slash', async () => {
    const dirPath = '/path/to/dir/';
    const mockDirectoryItems = [
      {
        path: `${dirPath}file1.txt`,
        gitObjectType: 'blob',
        isFolder: false,
      },
    ];

    // Even with one item, it should be treated as a directory due to trailing slash
    mockGitApi.getItems = jest.fn().mockResolvedValue(mockDirectoryItems);

    const result = await getFileContent(
      mockConnection,
      mockProjectId,
      mockRepositoryId,
      dirPath,
    );

    expect(result.isDirectory).toBe(true);
    expect(result.content).toBe(JSON.stringify(mockDirectoryItems, null, 2));
  });
});

```

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

```typescript
import { WebApi } from 'azure-devops-node-api';
import { searchWorkItems } from './feature';
import { getConnection } from '../../../server';
import { AzureDevOpsConfig } from '../../../shared/types';
import { AuthenticationMethod } from '../../../shared/auth';

// Skip tests if no PAT is available
const hasPat = process.env.AZURE_DEVOPS_PAT && process.env.AZURE_DEVOPS_ORG_URL;
const describeOrSkip = hasPat ? describe : describe.skip;

describeOrSkip('searchWorkItems (Integration)', () => {
  let connection: WebApi;
  let config: AzureDevOpsConfig;
  let projectId: string;

  beforeAll(async () => {
    // Set up the connection
    config = {
      organizationUrl: process.env.AZURE_DEVOPS_ORG_URL || '',
      authMethod: AuthenticationMethod.PersonalAccessToken,
      personalAccessToken: process.env.AZURE_DEVOPS_PAT || '',
      defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT || '',
    };

    connection = await getConnection(config);
    projectId = config.defaultProject || '';

    // Skip tests if no default project is set
    if (!projectId) {
      console.warn('Skipping integration tests: No default project set');
    }
  }, 30000);

  it('should search for work items', async () => {
    // Skip test if no default project
    if (!projectId) {
      return;
    }

    // Act
    const result = await searchWorkItems(connection, {
      searchText: 'test',
      projectId,
      top: 10,
      includeFacets: true,
    });

    // Assert
    expect(result).toBeDefined();
    expect(typeof result.count).toBe('number');
    expect(Array.isArray(result.results)).toBe(true);

    // If there are results, verify their structure
    if (result.results.length > 0) {
      const firstResult = result.results[0];
      expect(firstResult.project).toBeDefined();
      expect(firstResult.fields).toBeDefined();
      expect(firstResult.fields['system.id']).toBeDefined();
      expect(firstResult.fields['system.title']).toBeDefined();
      expect(firstResult.hits).toBeDefined();
      expect(firstResult.url).toBeDefined();
    }

    // If facets were requested, verify their structure
    if (result.facets) {
      expect(result.facets).toBeDefined();
    }
  }, 30000);

  it('should filter work items by type', async () => {
    // Skip test if no default project
    if (!projectId) {
      return;
    }

    // Act
    const result = await searchWorkItems(connection, {
      searchText: 'test',
      projectId,
      filters: {
        'System.WorkItemType': ['Bug'],
      },
      top: 10,
    });

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

    // If there are results, verify they are all bugs
    if (result.results.length > 0) {
      result.results.forEach((item) => {
        expect(item.fields['system.workitemtype'].toLowerCase()).toBe('bug');
      });
    }
  }, 30000);

  it('should support pagination', async () => {
    // Skip test if no default project
    if (!projectId) {
      return;
    }

    // Act - Get first page
    const firstPage = await searchWorkItems(connection, {
      searchText: 'test',
      projectId,
      top: 5,
      skip: 0,
    });

    // If there are enough results, test pagination
    if (firstPage.count > 5) {
      // Act - Get second page
      const secondPage = await searchWorkItems(connection, {
        searchText: 'test',
        projectId,
        top: 5,
        skip: 5,
      });

      // Assert
      expect(secondPage).toBeDefined();
      expect(secondPage.results).toBeDefined();

      // Verify the pages have different items
      if (firstPage.results.length > 0 && secondPage.results.length > 0) {
        const firstPageIds = firstPage.results.map(
          (r) => r.fields['system.id'],
        );
        const secondPageIds = secondPage.results.map(
          (r) => r.fields['system.id'],
        );

        // Check that the pages don't have overlapping IDs
        const overlap = firstPageIds.filter((id) => secondPageIds.includes(id));
        expect(overlap.length).toBe(0);
      }
    }
  }, 30000);

  it('should support sorting', async () => {
    // Skip test if no default project
    if (!projectId) {
      return;
    }

    // Act - Get results sorted by creation date (newest first)
    const result = await searchWorkItems(connection, {
      searchText: 'test',
      projectId,
      orderBy: [{ field: 'System.CreatedDate', sortOrder: 'DESC' }],
      top: 10,
    });

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

    // If there are multiple results, verify they are sorted
    if (result.results.length > 1) {
      const dates = result.results
        .filter((r) => r.fields['system.createddate'] !== undefined)
        .map((r) =>
          new Date(r.fields['system.createddate'] as string).getTime(),
        );

      // Check that dates are in descending order
      for (let i = 0; i < dates.length - 1; i++) {
        expect(dates[i]).toBeGreaterThanOrEqual(dates[i + 1]);
      }
    }
  }, 30000);

  // Add a test to verify Azure Identity authentication if configured
  if (
    process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'azure-identity'
  ) {
    test('should search work items using Azure Identity authentication', async () => {
      // Skip if required environment variables are missing
      if (!process.env.AZURE_DEVOPS_ORG_URL || !process.env.TEST_PROJECT_ID) {
        console.log('Skipping test: required environment variables missing');
        return;
      }

      // Create a config with Azure Identity authentication
      const testConfig: AzureDevOpsConfig = {
        organizationUrl: process.env.AZURE_DEVOPS_ORG_URL,
        authMethod: AuthenticationMethod.AzureIdentity,
        defaultProject: process.env.TEST_PROJECT_ID,
      };

      // Create the connection using the config
      const connection = await getConnection(testConfig);

      // Search work items
      const result = await searchWorkItems(connection, {
        projectId: process.env.TEST_PROJECT_ID,
        searchText: 'test',
      });

      // Check that the response is properly formatted
      expect(result).toBeDefined();
      expect(result.count).toBeDefined();
      expect(Array.isArray(result.results)).toBe(true);
    });
  }
});

```

--------------------------------------------------------------------------------
/docs/tools/work-items.md:
--------------------------------------------------------------------------------

```markdown
# Work Item Tools

This document describes the tools available for working with Azure DevOps work items.

## Table of Contents

- [`get_work_item`](#get_work_item) - Retrieve a specific work item by ID
- [`create_work_item`](#create_work_item) - Create a new work item
- [`list_work_items`](#list_work_items) - List work items in a project

## get_work_item

Retrieves a work item by its ID.

### Parameters

| Parameter    | Type   | Required | Description                                                                       |
| ------------ | ------ | -------- | --------------------------------------------------------------------------------- |
| `workItemId` | number | Yes      | The ID of the work item to retrieve                                               |
| `expand`     | string | No       | Controls the level of detail in the response. Defaults to "All" if not specified. Other values: "Relations", "Fields", "None" |

### Response

Returns a work item object with the following structure:

```json
{
  "id": 123,
  "fields": {
    "System.Title": "Sample Work Item",
    "System.State": "Active",
    "System.AssignedTo": "[email protected]",
    "System.Description": "Description of the work item"
  },
  "url": "https://dev.azure.com/organization/project/_apis/wit/workItems/123"
}
```

### Error Handling

- Returns `AzureDevOpsResourceNotFoundError` if the work item does not exist
- Returns `AzureDevOpsAuthenticationError` if authentication fails
- Returns generic error messages for other failures

### Example Usage

```javascript
// Using default expand="All"
const result = await callTool('get_work_item', {
  workItemId: 123,
});

// Explicitly specifying expand
const minimalResult = await callTool('get_work_item', {
  workItemId: 123,
  expand: 'None'
});
```

## create_work_item

Creates a new work item in a specified project.

### Parameters

| Parameter          | Type   | Required | Description                                                         |
| ------------------ | ------ | -------- | ------------------------------------------------------------------- |
| `projectId`        | string | Yes      | The ID or name of the project where the work item will be created   |
| `workItemType`     | string | Yes      | The type of work item to create (e.g., "Task", "Bug", "User Story") |
| `title`            | string | Yes      | The title of the work item                                          |
| `description`      | string | No       | The description of the work item                                    |
| `assignedTo`       | string | No       | The email or name of the user to assign the work item to            |
| `areaPath`         | string | No       | The area path for the work item                                     |
| `iterationPath`    | string | No       | The iteration path for the work item                                |
| `priority`         | number | No       | The priority of the work item                                       |
| `additionalFields` | object | No       | Additional fields to set on the work item (key-value pairs)         |

### Response

Returns the newly created work item object:

```json
{
  "id": 124,
  "fields": {
    "System.Title": "New Work Item",
    "System.State": "New",
    "System.Description": "Description of the new work item",
    "System.AssignedTo": "[email protected]",
    "System.AreaPath": "Project\\Team",
    "System.IterationPath": "Project\\Sprint 1",
    "Microsoft.VSTS.Common.Priority": 2
  },
  "url": "https://dev.azure.com/organization/project/_apis/wit/workItems/124"
}
```

### Error Handling

- Returns validation error if required fields are missing
- Returns `AzureDevOpsAuthenticationError` if authentication fails
- Returns `AzureDevOpsResourceNotFoundError` if the project does not exist
- Returns generic error messages for other failures

### Example Usage

```javascript
const result = await callTool('create_work_item', {
  projectId: 'my-project',
  workItemType: 'User Story',
  title: 'Implement login functionality',
  description:
    'Create a secure login system with email and password authentication',
  assignedTo: '[email protected]',
  priority: 1,
  additionalFields: {
    'Custom.Field': 'Custom Value',
  },
});
```

### Implementation Details

The tool creates a JSON patch document to define the fields of the work item, then calls the Azure DevOps API to create the work item. Each field is added to the document with an 'add' operation, and the document is submitted to the API.

## list_work_items

Lists work items in a specified project.

### Parameters

| Parameter   | Type   | Required | Description                                           |
| ----------- | ------ | -------- | ----------------------------------------------------- |
| `projectId` | string | Yes      | The ID or name of the project to list work items from |
| `teamId`    | string | No       | The ID of the team to list work items for             |
| `queryId`   | string | No       | ID of a saved work item query                         |
| `wiql`      | string | No       | Work Item Query Language (WIQL) query                 |
| `top`       | number | No       | Maximum number of work items to return                |
| `skip`      | number | No       | Number of work items to skip                          |

### Response

Returns an array of work item objects:

```json
[
  {
    "id": 123,
    "fields": {
      "System.Title": "Sample Work Item",
      "System.State": "Active",
      "System.AssignedTo": "[email protected]"
    },
    "url": "https://dev.azure.com/organization/project/_apis/wit/workItems/123"
  },
  {
    "id": 124,
    "fields": {
      "System.Title": "Another Work Item",
      "System.State": "New",
      "System.AssignedTo": "[email protected]"
    },
    "url": "https://dev.azure.com/organization/project/_apis/wit/workItems/124"
  }
]
```

### Error Handling

- Returns `AzureDevOpsResourceNotFoundError` if the project does not exist
- Returns `AzureDevOpsAuthenticationError` if authentication fails
- Returns generic error messages for other failures

### Example Usage

```javascript
const result = await callTool('list_work_items', {
  projectId: 'my-project',
  wiql: "SELECT [System.Id] FROM WorkItems WHERE [System.WorkItemType] = 'Task' ORDER BY [System.CreatedDate] DESC",
  top: 10,
});
```

```
Page 3/7FirstPrevNextLast