This is page 1 of 7. Use http://codebase.md/tiberriver256/azure-devops-mcp?lines=false&page={x} to view the full context.
# Directory Structure
```
├── .clinerules
├── .env.example
├── .eslintrc.json
├── .github
│ ├── 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
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
```
{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "lf"
}
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# TypeScript
dist/
*.tsbuildinfo
# Environment variables
.env
.env.local
.env.*.local
# IDE
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Test coverage
coverage/
# Logs
logs/
*.log
# Snyk Security Extension - AI Rules (auto-generated)
.cursor/rules/snyk_rules.mdc
# Patches
diffs/
```
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
```json
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"env": {
"node": true,
"es6": true,
"jest": true
},
"rules": {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": [
"error",
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
]
},
"overrides": [
{
"files": ["**/*.spec.unit.ts", "tests/**/*.ts"],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
}
}
],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"ignorePatterns": ["dist/**/*", "project-management/**/*"]
}
```
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
```
# Azure DevOps MCP Server - Environment Variables
# Azure DevOps Organization URL (required)
# e.g., https://dev.azure.com/your-organization
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
# Authentication Method (optional, defaults to 'azure-identity')
# Supported values: 'pat', 'azure-identity', 'azure-cli'
# - 'pat': Personal Access Token authentication
# - 'azure-identity': Azure Identity authentication (DefaultAzureCredential)
# - 'azure-cli': Azure CLI authentication (AzureCliCredential)
AZURE_DEVOPS_AUTH_METHOD=azure-identity
# Azure DevOps Personal Access Token (required for PAT authentication)
# Create one at: https://dev.azure.com/your-organization/_usersSettings/tokens
# Required scopes: Code (Read & Write), Work Items (Read & Write), Build (Read & Execute),
# Project and Team (Read), Graph (Read), Release (Read & Execute)
AZURE_DEVOPS_PAT=your-personal-access-token
# Default Project to use when not specified (optional)
AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project
# API Version to use (optional, defaults to latest)
# AZURE_DEVOPS_API_VERSION=6.0
# Note: This server uses stdio for communication, not HTTP
# The following variables are not used by the server but might be used by scripts:
# Logging Level (debug, info, warn, error)
LOG_LEVEL=info
# Azure Identity Credentials (for service principal authentication)
# Required only when using azure-identity with service principals
# AZURE_TENANT_ID=your-tenant-id
# AZURE_CLIENT_ID=your-client-id
# AZURE_CLIENT_SECRET=your-client-secret
```
--------------------------------------------------------------------------------
/.clinerules:
--------------------------------------------------------------------------------
```
# MCP Server Development Protocol
⚠️ CRITICAL: DO NOT USE attempt_completion BEFORE TESTING ⚠️
## Commit Rules
⚠️ MANDATORY: All git commits MUST adhere to the Conventional Commits specification (https://www.conventionalcommits.org/). Example: 'feat: implement user login' or 'fix: resolve calculation error'.
⚠️ RECOMMENDED: Use 'npm run commit' to create commit messages interactively, ensuring compliance.
## Step 1: Planning (PLAN MODE)
- What problem does this tool solve?
- What API/service will it use?
- What are the authentication requirements?
□ Standard API key
□ OAuth (requires separate setup script)
□ Other credentials
## Step 2: Implementation (ACT MODE)
1. Bootstrap
- For web services, JavaScript integration, or Node.js environments:
```bash
npx @modelcontextprotocol/create-server my-server
cd my-server
npm install
```
- For data science, ML workflows, or Python environments:
```bash
pip install mcp
# Or with uv (recommended)
uv add "mcp[cli]"
```
2. Core Implementation
- Use MCP SDK
- Implement comprehensive logging
- TypeScript (for web/JS projects):
```typescript
console.error('[Setup] Initializing server...');
console.error('[API] Request to endpoint:', endpoint);
console.error('[Error] Failed with:', error);
```
- Python (for data science/ML projects):
```python
import logging
logging.error('[Setup] Initializing server...')
logging.error(f'[API] Request to endpoint: {endpoint}')
logging.error(f'[Error] Failed with: {str(error)}')
```
- Add type definitions
- Handle errors with context
- Implement rate limiting if needed
3. Configuration
- Get credentials from user if needed
- Add to MCP settings:
- For TypeScript projects:
```json
{
"mcpServers": {
"my-server": {
"command": "node",
"args": ["path/to/build/index.js"],
"env": {
"API_KEY": "key"
},
"disabled": false,
"autoApprove": []
}
}
}
```
- For Python projects:
```bash
# Directly with command line
mcp install server.py -v API_KEY=key
# Or in settings.json
{
"mcpServers": {
"my-server": {
"command": "python",
"args": ["server.py"],
"env": {
"API_KEY": "key"
},
"disabled": false,
"autoApprove": []
}
}
}
```
## Step 3: Testing (BLOCKER ⛔️)
<thinking>
BEFORE using attempt_completion, I MUST verify:
□ Have I tested EVERY tool?
□ Have I confirmed success from the user for each test?
□ Have I documented the test results?
If ANY answer is "no", I MUST NOT use attempt_completion.
</thinking>
1. Test Each Tool (REQUIRED)
□ Test each tool with valid inputs
□ Verify output format is correct
⚠️ DO NOT PROCEED UNTIL ALL TOOLS TESTED
## Step 4: Completion
❗ STOP AND VERIFY:
□ Every tool has been tested with valid inputs
□ Output format is correct for each tool
Only after ALL tools have been tested can attempt_completion be used.
## Key Requirements
- ✓ Must use MCP SDK
- ✓ Must have comprehensive logging
- ✓ Must test each tool individually
- ✓ Must handle errors gracefully
- ⛔️ NEVER skip testing before completion
```
--------------------------------------------------------------------------------
/docs/examples/README.md:
--------------------------------------------------------------------------------
```markdown
# Authentication Examples
This directory contains example `.env` files for different authentication methods supported by the Azure DevOps MCP Server.
## Available Examples
1. **[pat-authentication.env](./pat-authentication.env)** - Example configuration for Personal Access Token (PAT) authentication
2. **[azure-identity-authentication.env](./azure-identity-authentication.env)** - Example configuration for Azure Identity (DefaultAzureCredential) authentication
3. **[azure-cli-authentication.env](./azure-cli-authentication.env)** - Example configuration for Azure CLI authentication
## How to Use These Examples
1. Choose the authentication method that best suits your needs
2. Copy the corresponding example file to the root of your project as `.env`
3. Replace the placeholder values with your actual values
4. Start the Azure DevOps MCP Server
For example:
```bash
# Copy the PAT authentication example
cp docs/examples/pat-authentication.env .env
# Edit the .env file with your values
nano .env
# Start the server
npm start
```
## Additional Resources
For more detailed information about authentication methods, setup instructions, and troubleshooting, refer to the [Authentication Guide](../authentication.md).
```
--------------------------------------------------------------------------------
/docs/testing/README.md:
--------------------------------------------------------------------------------
```markdown
# Testing Trophy Approach
## Overview
This project follows the Testing Trophy approach advocated by Kent C. Dodds instead of the traditional Testing Pyramid. The Testing Trophy emphasizes tests that provide higher confidence with less maintenance cost, focusing on how users actually interact with our software.

## Key Principles
1. **"The more your tests resemble the way your software is used, the more confidence they can give you."** - Kent C. Dodds
2. Focus on testing behavior and interfaces rather than implementation details
3. Maximize return on investment where "return" is confidence and "investment" is time
4. Use arrange/act/assert pattern for all tests
5. Co-locate tests with the code they test following Feature Sliced Design
## Test Types
### Static Analysis (The Base)
- TypeScript for type checking
- ESLint for code quality and consistency
- Runtime type checking with Zod
- Formatter (Prettier)
These tools catch many issues before tests are even run and provide immediate feedback during development.
### Unit Tests (Small Layer)
- Located in `*.spec.unit.ts` files
- Co-located with the code they test
- Focus on testing complex business logic in isolation
- Minimal mocking where necessary
- Run with `npm run test:unit`
Unit tests should be used sparingly for complex logic that requires isolated testing. We don't aim for 100% coverage with unit tests.
### Integration Tests (Main Focus)
- Located in `*.spec.int.ts` files
- Co-located with the features they test
- Test how modules work together
- Focus on testing behavior, not implementation
- Run with `npm run test:int`
These provide the bulk of our test coverage and confidence. They verify that different parts of the system work together correctly.
### End-to-End Tests (Small Cap)
- Located in `*.spec.e2e.ts` files
- **Only exists at the server level** (e.g., `server.spec.e2e.ts`) where they use the MCP client
- Test complete user flows across the entire application
- Provide the highest confidence but are slower and more costly to maintain
- Run with `npm run test:e2e`
End-to-end tests should only be created for critical user journeys that span the entire application. They should use the MCP client from `@modelcontextprotocol/sdk` to test the server as a black box, similar to how real users would interact with it.
For testing interactions with external APIs like Azure DevOps, use integration tests (`*.spec.int.ts`) instead, which are co-located with the feature implementations.
## Test File Naming Convention
- `*.spec.unit.ts` - For minimal unit tests (essential logic only)
- `*.spec.int.ts` - For integration tests (main focus)
- `*.spec.e2e.ts` - For end-to-end tests
## Test Location
We co-locate unit and integration tests with the code they're testing following Feature Sliced Design principles:
```
src/
features/
feature-name/
feature.ts
feature.spec.unit.ts # Unit tests
feature.spec.int.ts # Integration tests
```
E2E tests are only located at the server level since they test the full application:
```
src/
server.ts
server.spec.e2e.ts # E2E tests using the MCP client
```
This way, tests stay close to the code they're testing, making it easier to:
- Find tests when working on a feature
- Understand the relationship between tests and code
- Refactor code and tests together
- Maintain consistency between implementations and tests
## The Arrange/Act/Assert Pattern
All tests should follow the Arrange/Act/Assert pattern:
```typescript
test('should do something', () => {
// Arrange - set up the test
const input = 'something';
// Act - perform the action being tested
const result = doSomething(input);
// Assert - check that the action had the expected result
expect(result).toBe('expected output');
});
```
## Running Tests
- Run all tests: `npm test`
- Run unit tests: `npm run test:unit`
- Run integration tests: `npm run test:int`
- Run E2E tests: `npm run test:e2e`
## CI/CD Integration
Our CI/CD pipeline runs all test levels to ensure code quality:
1. Static analysis with TypeScript and ESLint
2. Unit tests
3. Integration tests
4. End-to-end tests
## Best Practices
1. Focus on integration tests for the bulk of your test coverage
2. Write unit tests only for complex business logic
3. Avoid testing implementation details
4. Use real dependencies when possible rather than mocks
5. Keep E2E tests focused on critical user flows
6. Use the arrange/act/assert pattern consistently
7. Co-locate tests with the code they're testing
## References
- [The Testing Trophy and Testing Classifications](https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications) by Kent C. Dodds
- [Testing of Microservices](https://engineering.atspotify.com/2018/01/testing-of-microservices/) (Testing Honeycomb approach) by Spotify Engineering
- [Feature Sliced Design](https://feature-sliced.design/) for co-location of tests with feature implementations
```
--------------------------------------------------------------------------------
/docs/tools/README.md:
--------------------------------------------------------------------------------
```markdown
# Azure DevOps MCP Server Tools Documentation
This directory contains documentation for all tools available in the Azure DevOps MCP server. Each tool is documented with examples, parameters, response formats, and error handling information.
## Navigation
- [Core Navigation Tools](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/core-navigation.md) - Overview of tools for navigating Azure DevOps resources
- [Organizations](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/organizations.md) - Tools for working with organizations
- [Projects](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/projects.md) - Tools for working with projects
- [Repositories](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/repositories.md) - Tools for working with Git repositories
- [Pull Requests](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pull-requests.md) - Tools for working with pull requests
- [Work Items](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/work-items.md) - Tools for working with work items
- [Pipelines](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pipelines.md) - Tools for working with pipelines
- [Resource URIs](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/resources.md) - Documentation for accessing repository content via resource URIs
## Tools by Category
### Organization Tools
- [`list_organizations`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/organizations.md#list_organizations) - List all Azure DevOps organizations accessible to the user
### Project Tools
- [`list_projects`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/projects.md#list_projects) - List all projects in the organization
- [`get_project`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/projects.md#get_project) - Get details of a specific project
### Repository Tools
- [`list_repositories`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/repositories.md#list_repositories) - List all repositories in a project
- [`get_repository`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/repositories.md#get_repository) - Get details of a specific repository
- [`get_repository_details`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/repositories.md#get_repository_details) - Get detailed information about a repository
- [`get_file_content`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/repositories.md#get_file_content) - Get content of a file or directory from a repository
### Pull Request Tools
- [`create_pull_request`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pull-requests.md#create_pull_request) - Create a new pull request
- [`list_pull_requests`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pull-requests.md#list_pull_requests) - List pull requests in a repository
- [`add_pull_request_comment`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pull-requests.md#add_pull_request_comment) - Add a comment to a pull request
- [`get_pull_request_comments`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pull-requests.md#get_pull_request_comments) - Get comments from a pull request
- [`update_pull_request`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pull-requests.md#update_pull_request) - Update an existing pull request (title, description, status, draft state, reviewers, work items)
### Work Item Tools
- [`get_work_item`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/work-items.md#get_work_item) - Retrieve a work item by ID
- [`create_work_item`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/work-items.md#create_work_item) - Create a new work item
- [`list_work_items`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/work-items.md#list_work_items) - List work items in a project
### Pipeline Tools
- [`list_pipelines`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pipelines.md#list_pipelines) - List all pipelines in a project
- [`get_pipeline`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pipelines.md#get_pipeline) - Get details of a specific pipeline
- [`list_pipeline_runs`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pipelines.md#list_pipeline_runs) - List recent runs for a pipeline with filters
- [`get_pipeline_run`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pipelines.md#get_pipeline_run) - Get detailed information about a specific run
- [`download_pipeline_artifact`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pipelines.md#download_pipeline_artifact) - Download a file from pipeline artifacts as text
- [`pipeline_timeline`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pipelines.md#pipeline_timeline) - Retrieve the stage/job timeline for a run
- [`get_pipeline_log`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pipelines.md#get_pipeline_log) - Retrieve log contents in plain or JSON formats
- [`trigger_pipeline`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pipelines.md#trigger_pipeline) - Trigger a pipeline run with customizable parameters
## Tool Structure
Each tool documentation follows a consistent structure:
1. **Description**: Brief explanation of what the tool does
2. **Parameters**: Required and optional parameters with explanations
3. **Response**: Expected response format with examples
4. **Error Handling**: Potential errors and how they're handled
5. **Example Usage**: Code examples showing how to use the tool
6. **Implementation Details**: Technical details about how the tool works
## Examples
Examples of using multiple tools together can be found in the [Core Navigation Tools](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/core-navigation.md#common-use-cases) documentation.
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Azure DevOps MCP Server
A Model Context Protocol (MCP) server implementation for Azure DevOps, allowing AI assistants to interact with Azure DevOps APIs through a standardized protocol.
## Overview
This server implements the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) for Azure DevOps, enabling AI assistants like Claude to interact with Azure DevOps resources securely. The server acts as a bridge between AI models and Azure DevOps APIs, providing a standardized way to:
- Access and manage projects, work items, repositories, and more
- Create and update work items, branches, and pull requests
- Execute common DevOps workflows through natural language
- Access repository content via standardized resource URIs
- Safely authenticate and interact with Azure DevOps resources
## Server Structure
The server is structured around the Model Context Protocol (MCP) for communicating with AI assistants. It provides tools for interacting with Azure DevOps resources including:
- Projects
- Work Items
- Repositories
- Pull Requests
- Branches
- Pipelines
### Core Components
- **AzureDevOpsServer**: Main server class that initializes the MCP server and registers tools
- **Feature Modules**: Organized by feature area (work-items, projects, repositories, etc.)
- **Request Handlers**: Each feature module provides request identification and handling functions
- **Tool Handlers**: Modular functions for each Azure DevOps operation
- **Configuration**: Environment-based configuration for organization URL, PAT, etc.
The server uses a feature-based architecture where each feature area (like work-items, projects, repositories) is encapsulated in its own module. This makes the codebase more maintainable and easier to extend with new features.
## Getting Started
### Prerequisites
- Node.js (v16+)
- npm or yarn
- Azure DevOps account with appropriate access
- Authentication credentials (see [Authentication Guide](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/authentication.md) for details):
- Personal Access Token (PAT), or
- Azure Identity credentials, or
- Azure CLI login
### Running with NPX
After building the project with `npm run build`, you can run the server directly with:
```bash
npx @tiberriver256/mcp-server-azure-devops
```
### Usage with Claude Desktop/Cursor AI
To integrate with Claude Desktop or Cursor AI, add one of the following configurations to your configuration file.
#### Azure Identity Authentication
Be sure you are logged in to Azure CLI with `az login` then add the following:
```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"
}
}
}
}
```
#### Personal Access Token (PAT) Authentication
```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": "pat",
"AZURE_DEVOPS_PAT": "<YOUR_PAT>",
"AZURE_DEVOPS_DEFAULT_PROJECT": "your-project-name"
}
}
}
}
```
For detailed configuration instructions and more authentication options, see the [Authentication Guide](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/authentication.md).
## Authentication Methods
This server supports multiple authentication methods for connecting to Azure DevOps APIs. For detailed setup instructions, configuration examples, and troubleshooting tips, see the [Authentication Guide](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/authentication.md).
### Supported Authentication Methods
1. **Personal Access Token (PAT)** - Simple token-based authentication
2. **Azure Identity (DefaultAzureCredential)** - Flexible authentication using the Azure Identity SDK
3. **Azure CLI** - Authentication using your Azure CLI login
Example configuration files for each authentication method are available in the [examples directory](https://github.com/tiberriver256/mcp-server-azure-devops/tree/main/docs/examples).
## Environment Variables
For a complete list of environment variables and their descriptions, see the [Authentication Guide](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/authentication.md#configuration-reference).
Key environment variables include:
| Variable | Description | Required | Default |
| ------------------------------ | ---------------------------------------------------------------------------------- | ---------------------------- | ---------------- |
| `AZURE_DEVOPS_AUTH_METHOD` | Authentication method (`pat`, `azure-identity`, or `azure-cli`) - case-insensitive | No | `azure-identity` |
| `AZURE_DEVOPS_ORG_URL` | Full URL to your Azure DevOps organization | Yes | - |
| `AZURE_DEVOPS_PAT` | Personal Access Token (for PAT auth) | Only with PAT auth | - |
| `AZURE_DEVOPS_DEFAULT_PROJECT` | Default project if none specified | No | - |
| `AZURE_DEVOPS_API_VERSION` | API version to use | No | Latest |
| `AZURE_TENANT_ID` | Azure AD tenant ID (for service principals) | Only with service principals | - |
| `AZURE_CLIENT_ID` | Azure AD application ID (for service principals) | Only with service principals | - |
| `AZURE_CLIENT_SECRET` | Azure AD client secret (for service principals) | Only with service principals | - |
| `LOG_LEVEL` | Logging level (debug, info, warn, error) | No | info |
## Troubleshooting Authentication
For detailed troubleshooting information for each authentication method, see the [Authentication Guide](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/authentication.md#troubleshooting-authentication-issues).
Common issues include:
- Invalid or expired credentials
- Insufficient permissions
- Network connectivity problems
- Configuration errors
## Authentication Implementation Details
For technical details about how authentication is implemented in the Azure DevOps MCP server, see the [Authentication Guide](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/authentication.md) and the source code in the `src/auth` directory.
## Available Tools
The Azure DevOps MCP server provides a variety of tools for interacting with Azure DevOps resources. For detailed documentation on each tool, please refer to the corresponding documentation.
### User Tools
- `get_me`: Get details of the authenticated user (id, displayName, email)
### Organization Tools
- `list_organizations`: List all accessible organizations
### Project Tools
- `list_projects`: List all projects in an organization
- `get_project`: Get details of a specific project
- `get_project_details`: Get comprehensive details of a project including process, work item types, and teams
### Repository Tools
- `list_repositories`: List all repositories in a project
- `get_repository`: Get details of a specific repository
- `get_repository_details`: Get detailed information about a repository including statistics and refs
- `get_file_content`: Get content of a file or directory from a repository
- `get_repository_tree`: List a repository's file tree from any path and depth
- `create_branch`: Create a new branch from an existing one
- `create_commit`: Commit multiple file changes to a branch using unified diffs or search/replace instructions
### Work Item Tools
- `get_work_item`: Retrieve a work item by ID
- `create_work_item`: Create a new work item
- `update_work_item`: Update an existing work item
- `list_work_items`: List work items in a project
- `manage_work_item_link`: Add, remove, or update links between work items
### Search Tools
- `search_code`: Search for code across repositories in a project
- `search_wiki`: Search for content across wiki pages in a project
- `search_work_items`: Search for work items across projects in Azure DevOps
### Pipelines Tools
- `list_pipelines`: List pipelines in a project
- `get_pipeline`: Get details of a specific pipeline
- `list_pipeline_runs`: List recent runs for a pipeline with optional filters
- `get_pipeline_run`: Get detailed run information and artifact summaries
- `download_pipeline_artifact`: Download a single artifact file as text
- `pipeline_timeline`: Retrieve the stage and job timeline for a run
- `get_pipeline_log`: Retrieve raw or JSON-formatted log content
- `trigger_pipeline`: Trigger a pipeline run with customizable parameters
### Wiki Tools
- `get_wikis`: List all wikis in a project
- `get_wiki_page`: Get content of a specific wiki page as plain text
### Pull Request Tools
- [`create_pull_request`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pull-requests.md#create_pull_request) - Create a new pull request
- [`list_pull_requests`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pull-requests.md#list_pull_requests) - List pull requests in a repository
- [`add_pull_request_comment`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pull-requests.md#add_pull_request_comment) - Add a comment to a pull request
- [`get_pull_request_comments`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pull-requests.md#get_pull_request_comments) - Get comments from a pull request
- [`update_pull_request`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pull-requests.md#update_pull_request) - Update an existing pull request (title, description, status, draft state, reviewers, work items)
- [`get_pull_request_changes`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pull-requests.md#get_pull_request_changes) - List changes in a pull request and policy evaluation status
- [`get_pull_request_checks`](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/docs/tools/pull-requests.md#get_pull_request_checks) - Summarize status checks, policy evaluations, and their related pipelines
For comprehensive documentation on all tools, see the [Tools Documentation](https://github.com/tiberriver256/mcp-server-azure-devops/tree/main/docs/tools).
## Contributing
Contributions are welcome! Please see [CONTRIBUTING.md](https://github.com/tiberriver256/mcp-server-azure-devops/blob/main/CONTRIBUTING.md) for contribution guidelines.
## Star History
[](https://www.star-history.com/#tiberriver256/mcp-server-azure-devops&Date)
## License
MIT
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
# Contributing to Azure DevOps MCP Server
We love your input! We want to make contributing to Azure DevOps MCP Server as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
- Becoming a maintainer
## Development Process
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
## Pull Requests
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (see Commit Message Guidelines below)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## Development Practices
This project follows Test-Driven Development practices. Each new feature should:
1. Begin with a failing test
2. Implement the minimal code to make the test pass
3. Refactor while keeping tests green
## Project Structure
The server is organized into feature-specific modules:
```
src/
└── features/
├── organizations/
├── pipelines/
├── projects/
├── pull-requests/
├── repositories/
├── search/
├── users/
├── wikis/
└── work-items/
```
Each feature module:
1. Exports its schemas, types, and individual tool functions
2. Provides an `is<FeatureName>Request` function to identify if a request is for this feature
3. Provides a `handle<FeatureName>Request` function to handle requests for this feature
### Adding a New Feature or Tool
When adding a new feature or tool:
1. Create a new directory under `src/features/` if needed
2. Implement your tool functions
3. Update the feature's `index.ts` to export your functions and add them to the request handlers
4. No changes to server.ts should be needed!
## Testing
### Unit Tests
Run unit tests with:
```bash
npm run test:unit
```
### Integration Tests
Integration tests require a connection to a real Azure DevOps instance. To run them:
1. Ensure your `.env` file is configured with valid Azure DevOps credentials:
```
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
AZURE_DEVOPS_PAT=your-personal-access-token
AZURE_DEVOPS_DEFAULT_PROJECT=your-project-name
```
2. Run the integration tests:
```bash
npm run test:integration
```
### CI Environment
For running tests in CI environments (like GitHub Actions), see [CI Environment Setup](docs/ci-setup.md) for instructions on configuring secrets.
## Commit Message Guidelines
We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification for our commit messages. This leads to more readable messages that are easy to follow when looking through the project history and enables automatic versioning and changelog generation.
### Commit Message Format
Each commit message consists of a **header**, a **body**, and a **footer**. The header has a special format that includes a **type**, a **scope**, and a **subject**:
```
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```
The **header** is mandatory, while the **scope** of the header is optional.
### Type
Must be one of the following:
- **feat**: A new feature
- **fix**: A bug fix
- **docs**: Documentation only changes
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, etc)
- **refactor**: A code change that neither fixes a bug nor adds a feature
- **perf**: A code change that improves performance
- **test**: Adding missing tests or correcting existing tests
- **build**: Changes that affect the build system or external dependencies
- **ci**: Changes to our CI configuration files and scripts
- **chore**: Other changes that don't modify src or test files
### Subject
The subject contains a succinct description of the change:
- Use the imperative, present tense: "change" not "changed" nor "changes"
- Don't capitalize the first letter
- No period (.) at the end
### Body
The body should include the motivation for the change and contrast this with previous behavior.
### Footer
The footer should contain any information about **Breaking Changes** and is also the place to reference GitHub issues that this commit closes.
Breaking Changes should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
### Using the Interactive Tool
To simplify the process of creating correctly formatted commit messages, we've set up a tool that will guide you through the process. Simply use:
```bash
npm run commit
```
This will start an interactive prompt that will help you generate a properly formatted commit message.
## Release Process
This project uses [Conventional Commits](https://www.conventionalcommits.org/) to automate versioning and changelog generation. When contributing, please follow the commit message convention.
To create a commit with the correct format, use:
```bash
npm run commit
```
## Automated Release Workflow
Our project uses [Release Please](https://github.com/googleapis/release-please) to automate releases based on Conventional Commits. This approach manages semantic versioning, changelog generation, and GitHub Releases creation.
The workflow is automatically triggered on pushes to the `main` branch and follows this process:
1. Release Please analyzes commit messages since the last release
2. If releasable changes are detected, it creates or updates a Release PR
3. When the Release PR is merged, it:
- Updates the version in package.json
- Updates CHANGELOG.md with details of all changes
- Creates a Git tag and GitHub Release
- Publishes the package to npm
### Release PR Process
1. When commits with conventional commit messages are pushed to `main`, Release Please automatically creates a Release PR
2. The Release PR contains all the changes since the last release with proper version bump based on commit types:
- `feat:` commits trigger a minor version bump
- `fix:` commits trigger a patch version bump
- `feat!:` or `fix!:` commits with breaking changes trigger a major version bump
3. Review the Release PR to ensure the changelog and version bump are correct
4. Merge the Release PR to trigger the actual release
This automation ensures consistent and well-documented releases that accurately reflect the changes made since the previous release.
## License
By contributing, you agree that your contributions will be licensed under the project's license.
```
--------------------------------------------------------------------------------
/.github/release-please-manifest.json:
--------------------------------------------------------------------------------
```json
{
".": "0.1.43"
}
```
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
```yaml
github: Tiberriver256
```
--------------------------------------------------------------------------------
/src/types/diff.d.ts:
--------------------------------------------------------------------------------
```typescript
declare module 'diff';
```
--------------------------------------------------------------------------------
/src/shared/api/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './client';
```
--------------------------------------------------------------------------------
/src/shared/types/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './config';
```
--------------------------------------------------------------------------------
/src/features/pull-requests/add-pull-request-comment/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/pull-requests/get-pull-request-changes/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/pull-requests/get-pull-request-checks/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/pull-requests/get-pull-request-comments/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/pull-requests/update-pull-request/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/repositories/create-branch/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/repositories/create-commit/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/repositories/get-repository-tree/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/repositories/list-commits/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/search/search-wiki/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/search/search-work-items/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
```
--------------------------------------------------------------------------------
/src/shared/config/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './version';
```
--------------------------------------------------------------------------------
/src/shared/errors/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './azure-devops-errors';
```
--------------------------------------------------------------------------------
/src/features/organizations/list-organizations/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schema';
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/pipelines/download-pipeline-artifact/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
export * from './schema';
```
--------------------------------------------------------------------------------
/src/features/pipelines/get-pipeline-log/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
export * from './schema';
```
--------------------------------------------------------------------------------
/src/features/pipelines/get-pipeline-run/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
export * from './schema';
```
--------------------------------------------------------------------------------
/src/features/pipelines/get-pipeline/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
export * from './schema';
```
--------------------------------------------------------------------------------
/src/features/pipelines/list-pipeline-runs/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
export * from './schema';
```
--------------------------------------------------------------------------------
/src/features/pipelines/list-pipelines/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
export * from './schema';
```
--------------------------------------------------------------------------------
/src/features/pipelines/pipeline-timeline/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
export * from './schema';
```
--------------------------------------------------------------------------------
/src/features/projects/get-project/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schema';
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/projects/list-projects/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schema';
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/pull-requests/create-pull-request/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schema';
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/pull-requests/list-pull-requests/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
export * from './schema';
```
--------------------------------------------------------------------------------
/src/features/pull-requests/list-pull-requests/schema.ts:
--------------------------------------------------------------------------------
```typescript
export { ListPullRequestsSchema } from '../schemas';
```
--------------------------------------------------------------------------------
/src/features/repositories/get-all-repositories-tree/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schema';
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/repositories/get-file-content/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
export * from './schema';
```
--------------------------------------------------------------------------------
/src/features/repositories/get-repository-details/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schema';
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/repositories/get-repository/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schema';
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/repositories/list-repositories/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schema';
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/users/get-me/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schema';
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/work-items/create-work-item/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schema';
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/work-items/get-work-item/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schema';
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/work-items/list-work-items/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schema';
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/work-items/update-work-item/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schema';
export * from './feature';
```
--------------------------------------------------------------------------------
/src/features/search/search-code/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './feature';
export * from '../schemas';
```
--------------------------------------------------------------------------------
/src/features/users/get-me/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { GetMeSchema } from '../schemas';
export { GetMeSchema };
```
--------------------------------------------------------------------------------
/src/features/projects/get-project/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { GetProjectSchema } from '../schemas';
export { GetProjectSchema };
```
--------------------------------------------------------------------------------
/src/features/work-items/get-work-item/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { GetWorkItemSchema } from '../schemas';
export { GetWorkItemSchema };
```
--------------------------------------------------------------------------------
/src/features/projects/list-projects/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { ListProjectsSchema } from '../schemas';
export { ListProjectsSchema };
```
--------------------------------------------------------------------------------
/src/features/wikis/get-wikis/index.ts:
--------------------------------------------------------------------------------
```typescript
export { getWikis } from './feature';
export { GetWikisSchema } from './schema';
```
--------------------------------------------------------------------------------
/src/features/repositories/get-repository/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { GetRepositorySchema } from '../schemas';
export { GetRepositorySchema };
```
--------------------------------------------------------------------------------
/src/features/work-items/list-work-items/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { ListWorkItemsSchema } from '../schemas';
export { ListWorkItemsSchema };
```
--------------------------------------------------------------------------------
/src/features/work-items/create-work-item/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { CreateWorkItemSchema } from '../schemas';
export { CreateWorkItemSchema };
```
--------------------------------------------------------------------------------
/src/features/work-items/update-work-item/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { UpdateWorkItemSchema } from '../schemas';
export { UpdateWorkItemSchema };
```
--------------------------------------------------------------------------------
/src/features/wikis/get-wiki-page/index.ts:
--------------------------------------------------------------------------------
```typescript
export { getWikiPage } from './feature';
export { GetWikiPageSchema } from './schema';
```
--------------------------------------------------------------------------------
/src/features/repositories/list-repositories/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { ListRepositoriesSchema } from '../schemas';
export { ListRepositoriesSchema };
```
--------------------------------------------------------------------------------
/src/shared/config/version.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Current version of the Azure DevOps MCP server
*/
export const VERSION = '0.1.0';
```
--------------------------------------------------------------------------------
/src/features/organizations/list-organizations/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { ListOrganizationsSchema } from '../schemas';
export { ListOrganizationsSchema };
```
--------------------------------------------------------------------------------
/src/features/projects/get-project-details/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { GetProjectDetailsSchema } from '../schemas';
export { GetProjectDetailsSchema };
```
--------------------------------------------------------------------------------
/src/features/pull-requests/create-pull-request/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { CreatePullRequestSchema } from '../schemas';
export { CreatePullRequestSchema };
```
--------------------------------------------------------------------------------
/src/features/wikis/list-wiki-pages/index.ts:
--------------------------------------------------------------------------------
```typescript
export { ListWikiPagesSchema } from './schema';
export { listWikiPages } from './feature';
```
--------------------------------------------------------------------------------
/src/features/wikis/create-wiki-page/index.ts:
--------------------------------------------------------------------------------
```typescript
export { createWikiPage } from './feature';
export { CreateWikiPageSchema } from './schema';
```
--------------------------------------------------------------------------------
/src/features/work-items/manage-work-item-link/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { ManageWorkItemLinkSchema } from '../schemas';
export { ManageWorkItemLinkSchema };
```
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
```javascript
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
};
```
--------------------------------------------------------------------------------
/src/features/pipelines/trigger-pipeline/index.ts:
--------------------------------------------------------------------------------
```typescript
export { triggerPipeline } from './feature';
export { TriggerPipelineSchema } from './schema';
```
--------------------------------------------------------------------------------
/src/features/wikis/create-wiki/index.ts:
--------------------------------------------------------------------------------
```typescript
export { createWiki } from './feature';
export { CreateWikiSchema, WikiType } from './schema';
```
--------------------------------------------------------------------------------
/src/features/repositories/get-repository-details/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { GetRepositoryDetailsSchema } from '../schemas';
export { GetRepositoryDetailsSchema };
```
--------------------------------------------------------------------------------
/src/features/projects/get-project-details/index.ts:
--------------------------------------------------------------------------------
```typescript
export { GetProjectDetailsSchema } from './schema';
export { getProjectDetails } from './feature';
```
--------------------------------------------------------------------------------
/src/features/work-items/manage-work-item-link/index.ts:
--------------------------------------------------------------------------------
```typescript
export { manageWorkItemLink } from './feature';
export { ManageWorkItemLinkSchema } from './schema';
```
--------------------------------------------------------------------------------
/project-management/planning/project-plan.md:
--------------------------------------------------------------------------------
```markdown
# Project Plan
The project plan has been moved to:
https://github.com/users/Tiberriver256/projects/1
```
--------------------------------------------------------------------------------
/src/features/wikis/update-wiki-page/index.ts:
--------------------------------------------------------------------------------
```typescript
export { updateWikiPage, UpdateWikiPageOptions } from './feature';
export { UpdateWikiPageSchema } from './schema';
```
--------------------------------------------------------------------------------
/src/features/repositories/get-file-content/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { GetFileContentSchema } from '../schemas';
// Export with explicit name to avoid conflicts
export { GetFileContentSchema };
```
--------------------------------------------------------------------------------
/src/features/users/schemas.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
/**
* Schema for the get_me tool, which takes no parameters
*/
export const GetMeSchema = z.object({}).strict();
```
--------------------------------------------------------------------------------
/src/features/repositories/get-all-repositories-tree/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { GetAllRepositoriesTreeSchema } from '../schemas';
// Export with explicit name to avoid conflicts
export { GetAllRepositoriesTreeSchema };
```
--------------------------------------------------------------------------------
/src/features/organizations/schemas.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
/**
* Schema for the list organizations request
* Note: This is an empty schema because the operation doesn't require any parameters
*/
export const ListOrganizationsSchema = z.object({});
```
--------------------------------------------------------------------------------
/project-management/tdd-cycle.xml:
--------------------------------------------------------------------------------
```
<tdd-cycle>
<red>Write a failing test for new functionality.</red>
<green>Implement minimal code to pass the test.</green>
<refactor>Refactor code, ensuring tests pass.</refactor>
<repeat>Repeat for each new test.</repeat>
</tdd-cycle>
```
--------------------------------------------------------------------------------
/src/features/users/types.ts:
--------------------------------------------------------------------------------
```typescript
/**
* User profile information
*/
export interface UserProfile {
/**
* The ID of the user
*/
id: string;
/**
* The display name of the user
*/
displayName: string;
/**
* The email address of the user
*/
email: string;
}
```
--------------------------------------------------------------------------------
/src/shared/types/tool-definition.ts:
--------------------------------------------------------------------------------
```typescript
import { JsonSchema7Type } from 'zod-to-json-schema';
/**
* Represents a tool that can be listed in the ListTools response
*/
export interface ToolDefinition {
name: string;
description: string;
inputSchema: JsonSchema7Type;
mcp_enabled?: boolean;
}
```
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
```json
{
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.ts": "${capture}.spec.unit.ts, ${capture}.spec.int.ts, ${capture}.spec.e2e.ts"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.updateImportsOnFileMove.enabled": "always"
}
```
--------------------------------------------------------------------------------
/src/features/projects/types.ts:
--------------------------------------------------------------------------------
```typescript
import { TeamProject } from 'azure-devops-node-api/interfaces/CoreInterfaces';
/**
* Options for listing projects
*/
export interface ListProjectsOptions {
stateFilter?: number;
top?: number;
skip?: number;
continuationToken?: number;
}
// Re-export TeamProject type for convenience
export type { TeamProject };
```
--------------------------------------------------------------------------------
/src/features/organizations/types.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Organization interface
*/
export interface Organization {
/**
* The ID of the organization
*/
id: string;
/**
* The name of the organization
*/
name: string;
/**
* The URL of the organization
*/
url: string;
}
/**
* Azure DevOps resource ID for token acquisition
*/
export const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798';
```
--------------------------------------------------------------------------------
/jest.int.config.js:
--------------------------------------------------------------------------------
```javascript
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.spec.int.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverage: false,
verbose: true,
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
};
```
--------------------------------------------------------------------------------
/jest.unit.config.js:
--------------------------------------------------------------------------------
```javascript
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.spec.unit.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverage: false,
verbose: true,
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
};
```
--------------------------------------------------------------------------------
/.github/release-please-config.json:
--------------------------------------------------------------------------------
```json
{
"packages": {
".": {
"release-type": "node",
"package-name": "@tiberriver256/mcp-server-azure-devops",
"changelog-path": "CHANGELOG.md",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true,
"draft": false,
"prerelease": false
}
},
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
}
```
--------------------------------------------------------------------------------
/src/features/users/tool-definitions.ts:
--------------------------------------------------------------------------------
```typescript
import { zodToJsonSchema } from 'zod-to-json-schema';
import { ToolDefinition } from '../../shared/types/tool-definition';
import { GetMeSchema } from './schemas';
/**
* List of users tools
*/
export const usersTools: ToolDefinition[] = [
{
name: 'get_me',
description:
'Get details of the authenticated user (id, displayName, email)',
inputSchema: zodToJsonSchema(GetMeSchema),
},
];
```
--------------------------------------------------------------------------------
/src/shared/auth/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Authentication module for Azure DevOps
*
* This module provides authentication functionality for Azure DevOps API.
* It supports multiple authentication methods:
* - Personal Access Token (PAT)
* - Azure Identity (DefaultAzureCredential)
* - Azure CLI (AzureCliCredential)
*/
export {
AuthenticationMethod,
AuthConfig,
createAuthClient,
} from './auth-factory';
export { AzureDevOpsClient } from './client-factory';
```
--------------------------------------------------------------------------------
/src/features/organizations/tool-definitions.ts:
--------------------------------------------------------------------------------
```typescript
import { zodToJsonSchema } from 'zod-to-json-schema';
import { ToolDefinition } from '../../shared/types/tool-definition';
import { ListOrganizationsSchema } from './schemas';
/**
* List of organizations tools
*/
export const organizationsTools: ToolDefinition[] = [
{
name: 'list_organizations',
description:
'List all Azure DevOps organizations accessible to the current authentication',
inputSchema: zodToJsonSchema(ListOrganizationsSchema),
},
];
```
--------------------------------------------------------------------------------
/src/features/pipelines/get-pipeline-run/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject } from '../../../utils/environment';
export const GetPipelineRunSchema = z.object({
projectId: z
.string()
.optional()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
runId: z.number().int().min(1).describe('Pipeline run identifier'),
pipelineId: z
.number()
.int()
.min(1)
.optional()
.describe('Optional guard; validates the run belongs to this pipeline'),
});
```
--------------------------------------------------------------------------------
/src/features/wikis/get-wikis/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject, defaultOrg } from '../../../utils/environment';
/**
* Schema for listing wikis in an Azure DevOps project or organization
*/
export const GetWikisSchema = z.object({
organizationId: z
.string()
.optional()
.nullable()
.describe(`The ID or name of the organization (Default: ${defaultOrg})`),
projectId: z
.string()
.optional()
.nullable()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
});
```
--------------------------------------------------------------------------------
/jest.e2e.config.js:
--------------------------------------------------------------------------------
```javascript
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.spec.e2e.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverage: false,
verbose: true,
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
testTimeout: 30000, // Longer timeout for E2E tests
passWithNoTests: true, // Allow tests to pass when no tests exist yet
};
```
--------------------------------------------------------------------------------
/.github/skills/azure-devops-rest-api/scripts/clone_specs.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/bash
# Clone or update the vsts-rest-api-specs repository
REPO_DIR="/tmp/vsts-rest-api-specs"
if [ -d "$REPO_DIR" ]; then
echo "Updating existing vsts-rest-api-specs repository..."
cd "$REPO_DIR"
git pull
else
echo "Cloning vsts-rest-api-specs repository..."
cd /tmp
git clone --depth 1 https://github.com/MicrosoftDocs/vsts-rest-api-specs.git
fi
echo "✅ vsts-rest-api-specs available at: $REPO_DIR"
echo ""
echo "API areas available:"
ls -d "$REPO_DIR/specification"/*/ | xargs -n1 basename | sort
```
--------------------------------------------------------------------------------
/.kilocode/mcp.json:
--------------------------------------------------------------------------------
```json
{
"mcpServers": {
"shrimp-task-manager": {
"command": "npx",
"args": [
"-y",
"mcp-shrimp-task-manager"
],
"env": {
"DATA_DIR": "D:/mcp-server-azure-devops",
"TEMPLATES_USE": "en",
"ENABLE_GUI": "false"
},
"alwaysAllow": [
"init_project_rules",
"process_thought",
"plan_task",
"analyze_task",
"reflect_task",
"split_tasks",
"execute_task",
"verify_task",
"get_task_detail"
]
}
}
}
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "dist",
"sourceMap": true,
"declaration": true,
"strictNullChecks": true,
"noImplicitAny": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/src/features/pipelines/list-pipelines/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject } from '../../../utils/environment';
/**
* Schema for the listPipelines function
*/
export const ListPipelinesSchema = z.object({
// The project to list pipelines from
projectId: z
.string()
.optional()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
// Maximum number of pipelines to return
top: z.number().optional().describe('Maximum number of pipelines to return'),
// Order by field and direction
orderBy: z
.string()
.optional()
.describe('Order by field and direction (e.g., "createdDate desc")'),
});
```
--------------------------------------------------------------------------------
/src/features/wikis/get-wiki-page/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject, defaultOrg } from '../../../utils/environment';
/**
* Schema for getting a wiki page from an Azure DevOps wiki
*/
export const GetWikiPageSchema = z.object({
organizationId: z
.string()
.optional()
.nullable()
.describe(`The ID or name of the organization (Default: ${defaultOrg})`),
projectId: z
.string()
.optional()
.nullable()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
wikiId: z.string().describe('The ID or name of the wiki'),
pagePath: z.string().describe('The path of the page within the wiki'),
});
```
--------------------------------------------------------------------------------
/src/features/wikis/list-wiki-pages/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject, defaultOrg } from '../../../utils/environment';
/**
* Schema for listing wiki pages from an Azure DevOps wiki
*/
export const ListWikiPagesSchema = z.object({
organizationId: z
.string()
.optional()
.nullable()
.describe(`The ID or name of the organization (Default: ${defaultOrg})`),
projectId: z
.string()
.optional()
.nullable()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
wikiId: z.string().describe('The ID or name of the wiki'),
});
export type ListWikiPagesOptions = z.infer<typeof ListWikiPagesSchema>;
```
--------------------------------------------------------------------------------
/src/features/pipelines/download-pipeline-artifact/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject } from '../../../utils/environment';
export const DownloadPipelineArtifactSchema = z.object({
projectId: z
.string()
.optional()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
runId: z.number().int().min(1).describe('Pipeline run identifier'),
artifactPath: z
.string()
.min(1)
.describe(
'Path to the desired file inside the artifact (format: <artifactName>/<path/to/file>)',
),
pipelineId: z
.number()
.int()
.min(1)
.optional()
.describe('Optional guard; validates the run belongs to this pipeline'),
});
```
--------------------------------------------------------------------------------
/src/shared/types/request-handler.ts:
--------------------------------------------------------------------------------
```typescript
import {
CallToolRequest,
CallToolResult,
} from '@modelcontextprotocol/sdk/types.js';
import { WebApi } from 'azure-devops-node-api';
/**
* Function type for identifying if a request belongs to a specific feature.
*/
export interface RequestIdentifier {
(request: CallToolRequest): boolean;
}
/**
* Function type for handling feature-specific requests.
* Returns either the standard MCP CallToolResult or a simplified response structure
* for backward compatibility.
*/
export interface RequestHandler {
(
connection: WebApi,
request: CallToolRequest,
): Promise<
CallToolResult | { content: Array<{ type: string; text: string }> }
>;
}
```
--------------------------------------------------------------------------------
/src/features/pipelines/get-pipeline/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject } from '../../../utils/environment';
/**
* Schema for the getPipeline function
*/
export const GetPipelineSchema = z.object({
// The project containing the pipeline
projectId: z
.string()
.optional()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
// The ID of the pipeline to retrieve
pipelineId: z
.number()
.int()
.positive()
.describe('The numeric ID of the pipeline to retrieve'),
// The version of the pipeline to retrieve
pipelineVersion: z
.number()
.int()
.positive()
.optional()
.describe(
'The version of the pipeline to retrieve (latest if not specified)',
),
});
```
--------------------------------------------------------------------------------
/src/shared/types/config.ts:
--------------------------------------------------------------------------------
```typescript
import { AuthenticationMethod } from '../auth/auth-factory';
/**
* Azure DevOps configuration type definition
*/
export interface AzureDevOpsConfig {
/**
* The Azure DevOps organization URL (e.g., https://dev.azure.com/organization)
*/
organizationUrl: string;
/**
* Authentication method to use (pat, azure-identity, azure-cli)
* @default 'azure-identity'
*/
authMethod?: AuthenticationMethod;
/**
* Personal Access Token for authentication (required for PAT authentication)
*/
personalAccessToken?: string;
/**
* Optional default project to use when not specified
*/
defaultProject?: string;
/**
* Optional API version to use (defaults to latest)
*/
apiVersion?: string;
}
```
--------------------------------------------------------------------------------
/src/features/search/tool-definitions.ts:
--------------------------------------------------------------------------------
```typescript
import { zodToJsonSchema } from 'zod-to-json-schema';
import { ToolDefinition } from '../../shared/types/tool-definition';
import {
SearchCodeSchema,
SearchWikiSchema,
SearchWorkItemsSchema,
} from './schemas';
/**
* List of search tools
*/
export const searchTools: ToolDefinition[] = [
{
name: 'search_code',
description: 'Search for code across repositories in a project',
inputSchema: zodToJsonSchema(SearchCodeSchema),
},
{
name: 'search_wiki',
description: 'Search for content across wiki pages in a project',
inputSchema: zodToJsonSchema(SearchWikiSchema),
},
{
name: 'search_work_items',
description: 'Search for work items across projects in Azure DevOps',
inputSchema: zodToJsonSchema(SearchWorkItemsSchema),
},
];
```
--------------------------------------------------------------------------------
/src/features/projects/tool-definitions.ts:
--------------------------------------------------------------------------------
```typescript
import { zodToJsonSchema } from 'zod-to-json-schema';
import { ToolDefinition } from '../../shared/types/tool-definition';
import {
ListProjectsSchema,
GetProjectSchema,
GetProjectDetailsSchema,
} from './schemas';
/**
* List of projects tools
*/
export const projectsTools: ToolDefinition[] = [
{
name: 'list_projects',
description: 'List all projects in an organization',
inputSchema: zodToJsonSchema(ListProjectsSchema),
},
{
name: 'get_project',
description: 'Get details of a specific project',
inputSchema: zodToJsonSchema(GetProjectSchema),
},
{
name: 'get_project_details',
description:
'Get comprehensive details of a project including process, work item types, and teams',
inputSchema: zodToJsonSchema(GetProjectDetailsSchema),
},
];
```
--------------------------------------------------------------------------------
/docs/examples/pat-authentication.env:
--------------------------------------------------------------------------------
```
# Example .env file for Personal Access Token (PAT) authentication
# Replace the values with your own
# Authentication method (required)
AZURE_DEVOPS_AUTH_METHOD=pat
# Azure DevOps organization URL (required)
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
# Personal Access Token (required for PAT authentication)
# Create one at: https://dev.azure.com/your-organization/_usersSettings/tokens
AZURE_DEVOPS_PAT=your-personal-access-token
# Default project to use when not specified (optional)
AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project
# API Version to use (optional, defaults to latest)
# AZURE_DEVOPS_API_VERSION=6.0
# Logging Level (optional)
LOG_LEVEL=info
# Note: This server uses stdio for communication with the MCP client,
# not HTTP. It does not listen on a network port.
```
--------------------------------------------------------------------------------
/.github/skills/skill-creator/references/workflows.md:
--------------------------------------------------------------------------------
```markdown
# Workflow Patterns
## Sequential Workflows
For complex tasks, break operations into clear, sequential steps. It is often helpful to give Claude an overview of the process towards the beginning of SKILL.md:
```markdown
Filling a PDF form involves these steps:
1. Analyze the form (run analyze_form.py)
2. Create field mapping (edit fields.json)
3. Validate mapping (run validate_fields.py)
4. Fill the form (run fill_form.py)
5. Verify output (run verify_output.py)
```
## Conditional Workflows
For tasks with branching logic, guide Claude through decision points:
```markdown
1. Determine the modification type:
**Creating new content?** → Follow "Creation workflow" below
**Editing existing content?** → Follow "Editing workflow" below
2. Creation workflow: [steps]
3. Editing workflow: [steps]
```
```
--------------------------------------------------------------------------------
/docs/tools/user-tools.md:
--------------------------------------------------------------------------------
```markdown
# Azure DevOps User Tools
This document describes the user-related tools provided by the Azure DevOps MCP server.
## get_me
The `get_me` tool retrieves information about the currently authenticated user.
### Input
This tool doesn't require any input parameters.
```json
{}
```
### Output
The tool returns the user's profile information including:
- `id`: The unique identifier for the user
- `displayName`: The user's display name
- `email`: The user's email address
#### Example Response
```json
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"displayName": "John Doe",
"email": "[email protected]"
}
```
### Error Handling
The tool may return the following errors:
- `AzureDevOpsAuthenticationError`: If authentication fails
- `AzureDevOpsError`: For general errors when retrieving user information
```
--------------------------------------------------------------------------------
/src/features/wikis/update-wiki-page/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject, defaultOrg } from '../../../utils/environment';
/**
* Schema for validating wiki page update options
*/
export const UpdateWikiPageSchema = z.object({
organizationId: z
.string()
.optional()
.nullable()
.describe(`The ID or name of the organization (Default: ${defaultOrg})`),
projectId: z
.string()
.optional()
.nullable()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
wikiId: z.string().min(1).describe('The ID or name of the wiki'),
pagePath: z.string().min(1).describe('Path of the wiki page to update'),
content: z
.string()
.min(1)
.describe('The new content for the wiki page in markdown format'),
comment: z
.string()
.optional()
.nullable()
.describe('Optional comment for the update'),
});
```
--------------------------------------------------------------------------------
/src/features/work-items/__test__/fixtures.ts:
--------------------------------------------------------------------------------
```typescript
import { WorkItem } from '../types';
/**
* Standard work item fixture for tests
*/
export const createWorkItemFixture = (
id: number,
title: string = 'Test Work Item',
state: string = 'Active',
assignedTo?: string,
): WorkItem => {
return {
id,
rev: 1,
fields: {
'System.Id': id,
'System.Title': title,
'System.State': state,
...(assignedTo ? { 'System.AssignedTo': assignedTo } : {}),
},
url: `https://dev.azure.com/test-org/test-project/_apis/wit/workItems/${id}`,
} as WorkItem;
};
/**
* Create a collection of work items for list tests
*/
export const createWorkItemsFixture = (count: number = 3): WorkItem[] => {
return Array.from({ length: count }, (_, i) =>
createWorkItemFixture(
i + 1,
`Work Item ${i + 1}`,
i % 2 === 0 ? 'Active' : 'Resolved',
),
);
};
```
--------------------------------------------------------------------------------
/docs/examples/azure-cli-authentication.env:
--------------------------------------------------------------------------------
```
# Example .env file for Azure CLI authentication
# Replace the values with your own
# Authentication method (required)
AZURE_DEVOPS_AUTH_METHOD=azure-cli
# Azure DevOps organization URL (required)
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
# Default project to use when not specified (optional)
AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project
# API Version to use (optional, defaults to latest)
# AZURE_DEVOPS_API_VERSION=6.0
# Logging Level (optional)
LOG_LEVEL=info
# Note: This server uses stdio for communication with the MCP client,
# not HTTP. It does not listen on a network port.
# Note: Before using Azure CLI authentication, make sure you have:
# 1. Installed the Azure CLI (https://docs.microsoft.com/cli/azure/install-azure-cli)
# 2. Logged in with 'az login'
# 3. Verified your account has access to the Azure DevOps organization
```
--------------------------------------------------------------------------------
/src/features/repositories/list-repositories/feature.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { AzureDevOpsError } from '../../../shared/errors';
import { ListRepositoriesOptions, GitRepository } from '../types';
/**
* List repositories in a project
*
* @param connection The Azure DevOps WebApi connection
* @param options Parameters for listing repositories
* @returns Array of repositories
*/
export async function listRepositories(
connection: WebApi,
options: ListRepositoriesOptions,
): Promise<GitRepository[]> {
try {
const gitApi = await connection.getGitApi();
const repositories = await gitApi.getRepositories(
options.projectId,
options.includeLinks,
);
return repositories;
} catch (error) {
if (error instanceof AzureDevOpsError) {
throw error;
}
throw new Error(
`Failed to list repositories: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
```
--------------------------------------------------------------------------------
/src/features/projects/list-projects/feature.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { AzureDevOpsError } from '../../../shared/errors';
import { ListProjectsOptions, TeamProject } from '../types';
/**
* List all projects in the organization
*
* @param connection The Azure DevOps WebApi connection
* @param options Optional parameters for listing projects
* @returns Array of projects
*/
export async function listProjects(
connection: WebApi,
options: ListProjectsOptions = {},
): Promise<TeamProject[]> {
try {
const coreApi = await connection.getCoreApi();
const projects = await coreApi.getProjects(
options.stateFilter,
options.top,
options.skip,
options.continuationToken,
);
return projects;
} catch (error) {
if (error instanceof AzureDevOpsError) {
throw error;
}
throw new Error(
`Failed to list projects: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
```
--------------------------------------------------------------------------------
/src/features/pipelines/get-pipeline-log/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject } from '../../../utils/environment';
export const GetPipelineLogSchema = z.object({
projectId: z
.string()
.optional()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
runId: z.number().int().min(1).describe('Pipeline run identifier'),
logId: z
.number()
.int()
.min(1)
.describe('Log identifier from the timeline record'),
format: z
.enum(['plain', 'json'])
.optional()
.describe('Optional format for the log contents (plain or json)'),
startLine: z
.number()
.int()
.min(0)
.optional()
.describe('Optional starting line number for the log segment'),
endLine: z
.number()
.int()
.min(0)
.optional()
.describe('Optional ending line number for the log segment'),
pipelineId: z
.number()
.int()
.min(1)
.optional()
.describe('Optional pipeline numeric ID for reference only'),
});
```
--------------------------------------------------------------------------------
/src/features/pipelines/helpers.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { Build } from 'azure-devops-node-api/interfaces/BuildInterfaces';
export function coercePipelineId(id: unknown): number | undefined {
if (typeof id === 'number') {
return id;
}
if (typeof id === 'string') {
const parsed = Number.parseInt(id, 10);
return Number.isNaN(parsed) ? undefined : parsed;
}
return undefined;
}
export async function resolvePipelineId(
connection: WebApi,
projectId: string,
runId: number,
providedPipelineId?: number,
): Promise<number | undefined> {
if (typeof providedPipelineId === 'number') {
return providedPipelineId;
}
try {
const buildApi = await connection.getBuildApi();
const build = (await buildApi.getBuild(projectId, runId)) as
| Build
| undefined;
return coercePipelineId(build?.definition?.id);
} catch {
// Swallow errors here; we'll handle not-found later when the main request fails
return undefined;
}
}
```
--------------------------------------------------------------------------------
/src/features/work-items/__test__/test-utils.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Test utilities for work item tests
* These utilities help reduce test execution time and improve test reliability
*/
/**
* Times test execution to help identify slow tests
* @param testName Name of the test
* @param fn Test function to execute
*/
export async function timeTest(testName: string, fn: () => Promise<void>) {
const start = performance.now();
await fn();
const end = performance.now();
const duration = end - start;
if (duration > 100) {
console.warn(`Test "${testName}" is slow (${duration.toFixed(2)}ms)`);
}
return duration;
}
/**
* Setup function to prepare test environment
* Call at beginning of test to ensure consistent setup
*/
export function setupTestEnvironment() {
// Set any environment variables needed for tests
const originalEnv = { ...process.env };
return {
// Clean up function to restore environment
cleanup: () => {
// Restore original environment
process.env = originalEnv;
},
};
}
```
--------------------------------------------------------------------------------
/src/utils/environment.ts:
--------------------------------------------------------------------------------
```typescript
// Load environment variables
import dotenv from 'dotenv';
dotenv.config();
/**
* Utility functions and constants related to environment variables.
*/
/**
* Extract organization name from Azure DevOps organization URL
*/
export function getOrgNameFromUrl(url?: string): string {
if (!url) return 'unknown-organization';
const devMatch = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)/);
if (devMatch) {
return devMatch[1];
}
// Fallback only for Azure DevOps Server URLs
if (url.includes('azure')) {
const fallbackMatch = url.match(/https?:\/\/[^/]+\/([^/]+)/);
return fallbackMatch ? fallbackMatch[1] : 'unknown-organization';
}
return 'unknown-organization';
}
/**
* Default project name from environment variables
*/
export const defaultProject =
process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'no default project';
/**
* Default organization name derived from the organization URL
*/
export const defaultOrg = getOrgNameFromUrl(process.env.AZURE_DEVOPS_ORG_URL);
```
--------------------------------------------------------------------------------
/src/features/projects/get-project/feature.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import {
AzureDevOpsResourceNotFoundError,
AzureDevOpsError,
} from '../../../shared/errors';
import { TeamProject } from '../types';
/**
* Get a project by ID or name
*
* @param connection The Azure DevOps WebApi connection
* @param projectId The ID or name of the project
* @returns The project details
* @throws {AzureDevOpsResourceNotFoundError} If the project is not found
*/
export async function getProject(
connection: WebApi,
projectId: string,
): Promise<TeamProject> {
try {
const coreApi = await connection.getCoreApi();
const project = await coreApi.getProject(projectId);
if (!project) {
throw new AzureDevOpsResourceNotFoundError(
`Project '${projectId}' not found`,
);
}
return project;
} catch (error) {
if (error instanceof AzureDevOpsError) {
throw error;
}
throw new Error(
`Failed to get project: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
```
--------------------------------------------------------------------------------
/src/features/projects/__test__/test-helpers.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { getPersonalAccessTokenHandler } from 'azure-devops-node-api';
/**
* Creates a WebApi connection for tests with real credentials
*
* @returns WebApi connection
*/
export async function getTestConnection(): Promise<WebApi | null> {
// If we have real credentials, use them
const orgUrl = process.env.AZURE_DEVOPS_ORG_URL;
const token = process.env.AZURE_DEVOPS_PAT;
if (orgUrl && token) {
const authHandler = getPersonalAccessTokenHandler(token);
return new WebApi(orgUrl, authHandler);
}
// If we don't have credentials, return null
return null;
}
/**
* Determines if integration tests should be skipped
*
* @returns true if integration tests should be skipped
*/
export function shouldSkipIntegrationTest(): boolean {
if (!process.env.AZURE_DEVOPS_ORG_URL || !process.env.AZURE_DEVOPS_PAT) {
console.log(
'Skipping integration test: No real Azure DevOps connection available',
);
return true;
}
return false;
}
```
--------------------------------------------------------------------------------
/src/features/repositories/__test__/test-helpers.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { getPersonalAccessTokenHandler } from 'azure-devops-node-api';
/**
* Creates a WebApi connection for tests with real credentials
*
* @returns WebApi connection
*/
export async function getTestConnection(): Promise<WebApi | null> {
// If we have real credentials, use them
const orgUrl = process.env.AZURE_DEVOPS_ORG_URL;
const token = process.env.AZURE_DEVOPS_PAT;
if (orgUrl && token) {
const authHandler = getPersonalAccessTokenHandler(token);
return new WebApi(orgUrl, authHandler);
}
// If we don't have credentials, return null
return null;
}
/**
* Determines if integration tests should be skipped
*
* @returns true if integration tests should be skipped
*/
export function shouldSkipIntegrationTest(): boolean {
if (!process.env.AZURE_DEVOPS_ORG_URL || !process.env.AZURE_DEVOPS_PAT) {
console.log(
'Skipping integration test: No real Azure DevOps connection available',
);
return true;
}
return false;
}
```
--------------------------------------------------------------------------------
/src/features/work-items/__test__/test-helpers.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { getPersonalAccessTokenHandler } from 'azure-devops-node-api';
/**
* Creates a WebApi connection for tests with real credentials
*
* @returns WebApi connection
*/
export async function getTestConnection(): Promise<WebApi | null> {
// If we have real credentials, use them
const orgUrl = process.env.AZURE_DEVOPS_ORG_URL;
const token = process.env.AZURE_DEVOPS_PAT;
if (orgUrl && token) {
const authHandler = getPersonalAccessTokenHandler(token);
return new WebApi(orgUrl, authHandler);
}
// If we don't have credentials, return null
return null;
}
/**
* Determines if integration tests should be skipped
*
* @returns true if integration tests should be skipped
*/
export function shouldSkipIntegrationTest(): boolean {
if (!process.env.AZURE_DEVOPS_ORG_URL || !process.env.AZURE_DEVOPS_PAT) {
console.log(
'Skipping integration test: No real Azure DevOps connection available',
);
return true;
}
return false;
}
```
--------------------------------------------------------------------------------
/src/features/wikis/create-wiki-page/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject, defaultOrg } from '../../../utils/environment';
/**
* Schema for creating a new wiki page in Azure DevOps
*/
export const CreateWikiPageSchema = z.object({
organizationId: z
.string()
.optional()
.nullable()
.describe(`The ID or name of the organization (Default: ${defaultOrg})`),
projectId: z
.string()
.optional()
.nullable()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
wikiId: z.string().min(1).describe('The ID or name of the wiki'),
pagePath: z
.string()
.optional()
.nullable()
.default('/')
.describe(
'Path of the wiki page to create. If the path does not exist, it will be created. Defaults to the wiki root (/). Example: /ParentPage/NewPage',
),
content: z
.string()
.min(1)
.describe('The content for the new wiki page in markdown format'),
comment: z
.string()
.optional()
.describe('Optional comment for the creation or update'),
});
```
--------------------------------------------------------------------------------
/src/features/work-items/types.ts:
--------------------------------------------------------------------------------
```typescript
import {
WorkItem,
WorkItemReference,
} from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces';
/**
* Options for listing work items
*/
export interface ListWorkItemsOptions {
projectId: string;
teamId?: string;
queryId?: string;
wiql?: string;
top?: number;
skip?: number;
}
/**
* Options for creating a work item
*/
export interface CreateWorkItemOptions {
title: string;
description?: string;
assignedTo?: string;
areaPath?: string;
iterationPath?: string;
priority?: number;
parentId?: number;
additionalFields?: Record<string, string | number | boolean | null>;
}
/**
* Options for updating a work item
*/
export interface UpdateWorkItemOptions {
title?: string;
description?: string;
assignedTo?: string;
areaPath?: string;
iterationPath?: string;
priority?: number;
state?: string;
additionalFields?: Record<string, string | number | boolean | null>;
}
// Re-export WorkItem and WorkItemReference types for convenience
export type { WorkItem, WorkItemReference };
```
--------------------------------------------------------------------------------
/src/features/work-items/tool-definitions.ts:
--------------------------------------------------------------------------------
```typescript
import { zodToJsonSchema } from 'zod-to-json-schema';
import { ToolDefinition } from '../../shared/types/tool-definition';
import {
ListWorkItemsSchema,
CreateWorkItemSchema,
UpdateWorkItemSchema,
ManageWorkItemLinkSchema,
GetWorkItemSchema,
} from './schemas';
/**
* List of work items tools
*/
export const workItemsTools: ToolDefinition[] = [
{
name: 'list_work_items',
description: 'List work items in a project',
inputSchema: zodToJsonSchema(ListWorkItemsSchema),
},
{
name: 'get_work_item',
description: 'Get details of a specific work item',
inputSchema: zodToJsonSchema(GetWorkItemSchema),
},
{
name: 'create_work_item',
description: 'Create a new work item',
inputSchema: zodToJsonSchema(CreateWorkItemSchema),
},
{
name: 'update_work_item',
description: 'Update an existing work item',
inputSchema: zodToJsonSchema(UpdateWorkItemSchema),
},
{
name: 'manage_work_item_link',
description: 'Add or remove links between work items',
inputSchema: zodToJsonSchema(ManageWorkItemLinkSchema),
},
];
```
--------------------------------------------------------------------------------
/src/features/organizations/__test__/test-helpers.ts:
--------------------------------------------------------------------------------
```typescript
import { AzureDevOpsConfig } from '../../../shared/types';
import { AuthenticationMethod } from '../../../shared/auth';
/**
* Creates test configuration for Azure DevOps tests
*
* @returns Azure DevOps config
*/
export function getTestConfig(): AzureDevOpsConfig | null {
// If we have real credentials, use them
const orgUrl = process.env.AZURE_DEVOPS_ORG_URL;
const pat = process.env.AZURE_DEVOPS_PAT;
if (orgUrl && pat) {
return {
organizationUrl: orgUrl,
authMethod: AuthenticationMethod.PersonalAccessToken,
personalAccessToken: pat,
defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT,
};
}
// If we don't have credentials, return null
return null;
}
/**
* Determines if integration tests should be skipped
*
* @returns true if integration tests should be skipped
*/
export function shouldSkipIntegrationTest(): boolean {
if (!process.env.AZURE_DEVOPS_ORG_URL || !process.env.AZURE_DEVOPS_PAT) {
console.log(
'Skipping integration test: No real Azure DevOps connection available',
);
return true;
}
return false;
}
```
--------------------------------------------------------------------------------
/src/features/repositories/create-branch/feature.spec.unit.ts:
--------------------------------------------------------------------------------
```typescript
import { createBranch } from './feature';
import { AzureDevOpsError } from '../../../shared/errors';
describe('createBranch unit', () => {
test('should create branch when source exists', async () => {
const updateRefs = jest.fn().mockResolvedValue([{ success: true }]);
const mockConnection: any = {
getGitApi: jest.fn().mockResolvedValue({
getBranch: jest.fn().mockResolvedValue({ commit: { commitId: 'abc' } }),
updateRefs,
}),
};
await createBranch(mockConnection, {
projectId: 'p',
repositoryId: 'r',
sourceBranch: 'main',
newBranch: 'feature',
});
expect(updateRefs).toHaveBeenCalled();
});
test('should throw error when source branch missing', async () => {
const mockConnection: any = {
getGitApi: jest.fn().mockResolvedValue({
getBranch: jest.fn().mockResolvedValue(null),
}),
};
await expect(
createBranch(mockConnection, {
projectId: 'p',
repositoryId: 'r',
sourceBranch: 'missing',
newBranch: 'feature',
}),
).rejects.toThrow(AzureDevOpsError);
});
});
```
--------------------------------------------------------------------------------
/src/features/wikis/update-wiki-page/feature.ts:
--------------------------------------------------------------------------------
```typescript
import * as azureDevOpsClient from '../../../clients/azure-devops';
import { UpdateWikiPageSchema } from './schema';
import { z } from 'zod';
import { defaultOrg, defaultProject } from '../../../utils/environment';
/**
* Options for updating a wiki page
*/
export type UpdateWikiPageOptions = z.infer<typeof UpdateWikiPageSchema>;
/**
* Updates a wiki page in Azure DevOps
* @param options - The options for updating the wiki page
* @returns The updated wiki page
*/
export async function updateWikiPage(options: UpdateWikiPageOptions) {
const validatedOptions = UpdateWikiPageSchema.parse(options);
const { organizationId, projectId, wikiId, pagePath, content, comment } =
validatedOptions;
// Create the client
const client = await azureDevOpsClient.getWikiClient({
organizationId: organizationId ?? defaultOrg,
});
// Prepare the wiki page content
const wikiPageContent = {
content,
};
// Update the wiki page
const updatedPage = await client.updatePage(
wikiPageContent,
projectId ?? defaultProject,
wikiId,
pagePath,
{
comment: comment ?? undefined,
},
);
return updatedPage;
}
```
--------------------------------------------------------------------------------
/docs/examples/azure-identity-authentication.env:
--------------------------------------------------------------------------------
```
# Example .env file for Azure Identity (DefaultAzureCredential) authentication
# Replace the values with your own
# Authentication method (required)
AZURE_DEVOPS_AUTH_METHOD=azure-identity
# Azure DevOps organization URL (required)
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
# Default project to use when not specified (optional)
AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project
# API Version to use (optional, defaults to latest)
# AZURE_DEVOPS_API_VERSION=6.0
# Azure AD tenant ID (required for service principal authentication)
# AZURE_TENANT_ID=your-tenant-id
# Azure AD client ID (required for service principal authentication)
# AZURE_CLIENT_ID=your-client-id
# Azure AD client secret (required for service principal authentication)
# AZURE_CLIENT_SECRET=your-client-secret
# Logging Level (optional)
LOG_LEVEL=info
# Note: This server uses stdio for communication with the MCP client,
# not HTTP. It does not listen on a network port.
# Note: When using DefaultAzureCredential, you don't need to set AZURE_TENANT_ID,
# AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET if you're using other credential types
# like Managed Identity or Azure CLI.
```
--------------------------------------------------------------------------------
/src/features/repositories/get-repository/feature.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import {
AzureDevOpsResourceNotFoundError,
AzureDevOpsError,
} from '../../../shared/errors';
import { GitRepository } from '../types';
/**
* Get a repository by ID or name
*
* @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
* @returns The repository details
* @throws {AzureDevOpsResourceNotFoundError} If the repository is not found
*/
export async function getRepository(
connection: WebApi,
projectId: string,
repositoryId: string,
): Promise<GitRepository> {
try {
const gitApi = await connection.getGitApi();
const repository = await gitApi.getRepository(repositoryId, projectId);
if (!repository) {
throw new AzureDevOpsResourceNotFoundError(
`Repository '${repositoryId}' not found in project '${projectId}'`,
);
}
return repository;
} catch (error) {
if (error instanceof AzureDevOpsError) {
throw error;
}
throw new Error(
`Failed to get repository: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
```
--------------------------------------------------------------------------------
/src/features/users/index.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Users feature module
*
* This module contains user-related functionality.
*/
export * from './types';
export * from './get-me';
// 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 { getMe } from './';
/**
* Checks if the request is for the users feature
*/
export const isUsersRequest: RequestIdentifier = (
request: CallToolRequest,
): boolean => {
const toolName = request.params.name;
return ['get_me'].includes(toolName);
};
/**
* Handles users feature requests
*/
export const handleUsersRequest: RequestHandler = async (
connection: WebApi,
request: CallToolRequest,
): Promise<{ content: Array<{ type: string; text: string }> }> => {
switch (request.params.name) {
case 'get_me': {
const result = await getMe(connection);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
}
default:
throw new Error(`Unknown users tool: ${request.params.name}`);
}
};
```
--------------------------------------------------------------------------------
/src/features/wikis/get-wikis/feature.spec.int.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { getWikis } from './feature';
import {
getTestConnection,
shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';
describe('getWikis integration', () => {
let connection: WebApi | null = null;
let projectName: string;
beforeAll(async () => {
// Get a real connection using environment variables
connection = await getTestConnection();
projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
});
test('should retrieve wikis from Azure DevOps', async () => {
// Skip if no connection is available
if (shouldSkipIntegrationTest()) {
return;
}
// This connection must be available if we didn't skip
if (!connection) {
throw new Error(
'Connection should be available when test is not skipped',
);
}
// Get the wikis
const result = await getWikis(connection, {
projectId: projectName,
});
// Verify the result
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
if (result.length > 0) {
expect(result[0].name).toBeDefined();
expect(result[0].id).toBeDefined();
expect(result[0].type).toBeDefined();
}
});
});
```
--------------------------------------------------------------------------------
/src/features/users/get-me/feature.spec.int.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { getMe } from '../get-me';
import {
getTestConnection,
shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';
describe('getMe Integration', () => {
let connection: WebApi | null = null;
beforeAll(async () => {
// Get a real connection using environment variables
connection = await getTestConnection();
});
test('should get authenticated user profile information', async () => {
// Skip if no connection is available
if (shouldSkipIntegrationTest() || !connection) {
console.log('Skipping getMe integration test - no connection available');
return;
}
// Act - make a direct API call using Axios
const result = await getMe(connection);
// Assert on the actual response
expect(result).toBeDefined();
expect(result.id).toBeDefined();
expect(typeof result.id).toBe('string');
expect(result.displayName).toBeDefined();
expect(typeof result.displayName).toBe('string');
expect(result.displayName.length).toBeGreaterThan(0);
// Email should be defined, a string, and not empty
expect(result.email).toBeDefined();
expect(typeof result.email).toBe('string');
expect(result.email.length).toBeGreaterThan(0);
});
});
```
--------------------------------------------------------------------------------
/src/features/work-items/get-work-item/feature.spec.unit.ts:
--------------------------------------------------------------------------------
```typescript
import { getWorkItem } from './feature';
import { AzureDevOpsError } from '../../../shared/errors';
// Unit tests should only focus on isolated logic
// No real connections, HTTP requests, or dependencies
describe('getWorkItem unit', () => {
// Unit test for error handling logic - the only part that's suitable for a unit test
test('should propagate custom errors when thrown internally', async () => {
// Arrange - for unit test, we mock only what's needed
const mockConnection: any = {
getWorkItemTrackingApi: jest.fn().mockImplementation(() => {
throw new AzureDevOpsError('Custom error');
}),
};
// Act & Assert
await expect(getWorkItem(mockConnection, 123)).rejects.toThrow(
AzureDevOpsError,
);
await expect(getWorkItem(mockConnection, 123)).rejects.toThrow(
'Custom error',
);
});
test('should wrap unexpected errors in a friendly error message', async () => {
// Arrange
const mockConnection: any = {
getWorkItemTrackingApi: jest.fn().mockImplementation(() => {
throw new Error('Unexpected error');
}),
};
// Act & Assert
await expect(getWorkItem(mockConnection, 123)).rejects.toThrow(
'Failed to get work item: Unexpected error',
);
});
});
```
--------------------------------------------------------------------------------
/src/features/pipelines/trigger-pipeline/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject } from '../../../utils/environment';
/**
* Schema for the triggerPipeline function
*/
export const TriggerPipelineSchema = z.object({
// The project containing the pipeline
projectId: z
.string()
.optional()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
// The ID of the pipeline to trigger
pipelineId: z
.number()
.int()
.positive()
.describe('The numeric ID of the pipeline to trigger'),
// The branch to run the pipeline on
branch: z
.string()
.optional()
.describe(
'The branch to run the pipeline on (e.g., "main", "feature/my-branch"). If left empty, the default branch will be used',
),
// Variables to pass to the pipeline run
variables: z
.record(
z.object({
value: z.string(),
isSecret: z.boolean().optional(),
}),
)
.optional()
.describe('Variables to pass to the pipeline run'),
// Parameters for template-based pipelines
templateParameters: z
.record(z.string())
.optional()
.describe('Parameters for template-based pipelines'),
// Stages to skip in the pipeline run
stagesToSkip: z
.array(z.string())
.optional()
.describe('Stages to skip in the pipeline run'),
});
```
--------------------------------------------------------------------------------
/.github/workflows/release-please.yml:
--------------------------------------------------------------------------------
```yaml
name: Release Please
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
issues: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v4
id: release
with:
config-file: .github/release-please-config.json
manifest-file: .github/release-please-manifest.json
# The following steps only run if a new release is created
- name: Checkout code
if: ${{ steps.release.outputs.release_created }}
uses: actions/checkout@v3
with:
ref: ${{ steps.release.outputs.tag_name }}
- name: Setup Node.js
if: ${{ steps.release.outputs.release_created }}
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
registry-url: 'https://registry.npmjs.org/'
- name: Install Dependencies
if: ${{ steps.release.outputs.release_created }}
run: npm ci
- name: Build package
if: ${{ steps.release.outputs.release_created }}
run: npm run build
- name: Publish to npm
if: ${{ steps.release.outputs.release_created }}
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
```
--------------------------------------------------------------------------------
/src/features/pipelines/pipeline-timeline/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject } from '../../../utils/environment';
export const GetPipelineTimelineSchema = z.object({
projectId: z
.string()
.optional()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
runId: z.number().int().min(1).describe('Run identifier'),
timelineId: z
.string()
.optional()
.describe(
'Optional timeline identifier to select a specific timeline record',
),
pipelineId: z
.number()
.int()
.min(1)
.optional()
.describe('Optional pipeline numeric ID for reference only'),
state: z
.union([
z.enum(['pending', 'inProgress', 'completed']),
z.array(z.enum(['pending', 'inProgress', 'completed'])),
])
.optional()
.describe(
'Optional state filter (single value or array) applied to returned timeline records',
),
result: z
.union([
z.enum([
'succeeded',
'succeededWithIssues',
'failed',
'canceled',
'skipped',
'abandoned',
]),
z.array(
z.enum([
'succeeded',
'succeededWithIssues',
'failed',
'canceled',
'skipped',
'abandoned',
]),
),
])
.optional()
.describe(
'Optional result filter (single value or array) applied to returned timeline records',
),
});
```
--------------------------------------------------------------------------------
/src/features/repositories/create-branch/feature.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { GitRefUpdate } from 'azure-devops-node-api/interfaces/GitInterfaces';
import { AzureDevOpsError } from '../../../shared/errors';
import { CreateBranchOptions } from '../types';
/**
* Create a new branch from an existing one
*/
export async function createBranch(
connection: WebApi,
options: CreateBranchOptions,
): Promise<void> {
try {
const gitApi = await connection.getGitApi();
const source = await gitApi.getBranch(
options.repositoryId,
options.sourceBranch,
options.projectId,
);
const commitId = source?.commit?.commitId;
if (!commitId) {
throw new AzureDevOpsError(
`Source branch '${options.sourceBranch}' not found`,
);
}
const refUpdate: GitRefUpdate = {
name: `refs/heads/${options.newBranch}`,
oldObjectId: '0000000000000000000000000000000000000000',
newObjectId: commitId,
};
const result = await gitApi.updateRefs(
[refUpdate],
options.repositoryId,
options.projectId,
);
if (!result.every((r) => r.success)) {
throw new AzureDevOpsError('Failed to create new branch');
}
} catch (error) {
if (error instanceof AzureDevOpsError) {
throw error;
}
throw new Error(
`Failed to create branch: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
```
--------------------------------------------------------------------------------
/src/features/pipelines/list-pipeline-runs/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject } from '../../../utils/environment';
export const ListPipelineRunsSchema = z.object({
projectId: z
.string()
.optional()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
pipelineId: z.number().int().min(1).describe('Pipeline numeric ID'),
top: z
.number()
.int()
.min(1)
.max(100)
.default(50)
.describe('Maximum number of runs to return (1-100)'),
continuationToken: z
.string()
.optional()
.describe('Continuation token for pagination'),
branch: z
.string()
.optional()
.describe('Branch to filter by (e.g., "main" or "refs/heads/main")'),
state: z
.enum(['notStarted', 'inProgress', 'completed', 'cancelling', 'postponed'])
.optional()
.describe('Filter by current run state'),
result: z
.enum(['succeeded', 'partiallySucceeded', 'failed', 'canceled', 'none'])
.optional()
.describe('Filter by final run result'),
createdFrom: z
.string()
.datetime()
.optional()
.describe('Filter runs created at or after this time (ISO 8601)'),
createdTo: z
.string()
.datetime()
.optional()
.describe('Filter runs created at or before this time (ISO 8601)'),
orderBy: z
.enum(['createdDate desc', 'createdDate asc'])
.default('createdDate desc')
.describe('Sort order for run creation date'),
});
```
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
```yaml
name: CI
on:
pull_request_target:
branches: [main]
types: [opened, synchronize, reopened]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
environment: external-pr-check
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Use latest Node LTS
uses: actions/setup-node@v3
with:
node-version: 'lts/*'
- name: Install Dependencies
run: npm install
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Unit Tests
run: npm run test:unit
- name: Integration Tests
run: npm run test:int
env:
CI: 'true'
AZURE_DEVOPS_ORG_URL: ${{ secrets.AZURE_DEVOPS_ORG_URL }}
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
AZURE_DEVOPS_DEFAULT_PROJECT: ${{ secrets.AZURE_DEVOPS_DEFAULT_PROJECT }}
AZURE_DEVOPS_DEFAULT_REPOSITORY: eShopOnWeb
AZURE_DEVOPS_AUTH_METHOD: pat
- name: E2E Tests
run: npm run test:e2e
env:
CI: 'true'
AZURE_DEVOPS_ORG_URL: ${{ secrets.AZURE_DEVOPS_ORG_URL }}
AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }}
AZURE_DEVOPS_DEFAULT_PROJECT: ${{ secrets.AZURE_DEVOPS_DEFAULT_PROJECT }}
AZURE_DEVOPS_DEFAULT_REPOSITORY: eShopOnWeb
AZURE_DEVOPS_AUTH_METHOD: pat
```
--------------------------------------------------------------------------------
/src/features/wikis/tool-definitions.ts:
--------------------------------------------------------------------------------
```typescript
import { zodToJsonSchema } from 'zod-to-json-schema';
import { ToolDefinition } from '../../shared/types/tool-definition';
import { GetWikisSchema } from './get-wikis/schema';
import { GetWikiPageSchema } from './get-wiki-page/schema';
import { CreateWikiSchema } from './create-wiki/schema';
import { UpdateWikiPageSchema } from './update-wiki-page/schema';
import { ListWikiPagesSchema } from './list-wiki-pages/schema';
import { CreateWikiPageSchema } from './create-wiki-page/schema';
/**
* List of wikis tools
*/
export const wikisTools: ToolDefinition[] = [
{
name: 'get_wikis',
description: 'Get details of wikis in a project',
inputSchema: zodToJsonSchema(GetWikisSchema),
},
{
name: 'get_wiki_page',
description: 'Get the content of a wiki page',
inputSchema: zodToJsonSchema(GetWikiPageSchema),
},
{
name: 'create_wiki',
description: 'Create a new wiki in the project',
inputSchema: zodToJsonSchema(CreateWikiSchema),
},
{
name: 'update_wiki_page',
description: 'Update content of a wiki page',
inputSchema: zodToJsonSchema(UpdateWikiPageSchema),
},
{
name: 'list_wiki_pages',
description: 'List pages within an Azure DevOps wiki',
inputSchema: zodToJsonSchema(ListWikiPagesSchema),
},
{
name: 'create_wiki_page',
description:
'Create a new page in a wiki. If the page already exists at the specified path, it will be updated.',
inputSchema: zodToJsonSchema(CreateWikiPageSchema),
},
];
```
--------------------------------------------------------------------------------
/src/features/organizations/list-organizations/feature.spec.int.ts:
--------------------------------------------------------------------------------
```typescript
import { listOrganizations } from './feature';
import {
getTestConfig,
shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';
describe('listOrganizations integration', () => {
test('should list organizations accessible to the authenticated user', async () => {
// Skip if no credentials are available
if (shouldSkipIntegrationTest()) {
return;
}
// Get test configuration
const config = getTestConfig();
if (!config) {
throw new Error(
'Configuration should be available when test is not skipped',
);
}
// Act - make an actual API call to Azure DevOps
const result = await listOrganizations(config);
// Assert on the actual response
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThan(0);
// Check structure of returned organizations
const firstOrg = result[0];
expect(firstOrg.id).toBeDefined();
expect(firstOrg.name).toBeDefined();
expect(firstOrg.url).toBeDefined();
// The organization URL in the config should match one of the returned organizations
// Extract the organization name from the URL
const orgUrlParts = config.organizationUrl.split('/');
const configOrgName = orgUrlParts[orgUrlParts.length - 1];
// Find matching organization
const matchingOrg = result.find(
(org) => org.name.toLowerCase() === configOrgName.toLowerCase(),
);
expect(matchingOrg).toBeDefined();
});
});
```
--------------------------------------------------------------------------------
/src/features/pipelines/list-pipelines/feature.spec.int.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { listPipelines } from './feature';
import {
getTestConnection,
shouldSkipIntegrationTest,
} from '../../../shared/test/test-helpers';
describe('listPipelines integration', () => {
let connection: WebApi | null = null;
beforeAll(async () => {
// Get a real connection using environment variables
connection = await getTestConnection();
// TODO: Implement createPipeline functionality and create test pipelines here
// Currently there is no way to create pipelines, so we can't ensure data exists like in list-work-items tests
// In the future, we should add code similar to list-work-items to create test pipelines
});
it('should list pipelines in a project', async () => {
// Skip if no connection is available or no project specified
if (
shouldSkipIntegrationTest() ||
!connection ||
!process.env.AZURE_DEVOPS_DEFAULT_PROJECT
) {
console.log(
'Skipping listPipelines integration test - no connection or project available',
);
return;
}
const projectId = process.env.AZURE_DEVOPS_DEFAULT_PROJECT;
const pipelines = await listPipelines(connection, { projectId });
expect(Array.isArray(pipelines)).toBe(true);
// If there are pipelines, check their structure
if (pipelines.length > 0) {
const pipeline = pipelines[0];
expect(pipeline.id).toBeDefined();
expect(pipeline.name).toBeDefined();
expect(pipeline.folder).toBeDefined();
expect(pipeline.revision).toBeDefined();
expect(pipeline.url).toBeDefined();
}
});
});
```
--------------------------------------------------------------------------------
/src/features/wikis/create-wiki/schema.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject, defaultOrg } from '../../../utils/environment';
/**
* Wiki types for creating wiki
*/
export enum WikiType {
/**
* The wiki is published from a git repository
*/
CodeWiki = 'codeWiki',
/**
* The wiki is provisioned for the team project
*/
ProjectWiki = 'projectWiki',
}
/**
* Schema for creating a wiki in an Azure DevOps project
*/
export const CreateWikiSchema = z
.object({
organizationId: z
.string()
.optional()
.nullable()
.describe(`The ID or name of the organization (Default: ${defaultOrg})`),
projectId: z
.string()
.optional()
.nullable()
.describe(`The ID or name of the project (Default: ${defaultProject})`),
name: z.string().describe('The name of the new wiki'),
type: z
.nativeEnum(WikiType)
.optional()
.default(WikiType.ProjectWiki)
.describe('Type of wiki to create (projectWiki or codeWiki)'),
repositoryId: z
.string()
.optional()
.nullable()
.describe(
'The ID of the repository to associate with the wiki (required for codeWiki)',
),
mappedPath: z
.string()
.optional()
.nullable()
.default('/')
.describe(
'Folder path inside repository which is shown as Wiki (only for codeWiki)',
),
})
.refine(
(data) => {
// If type is codeWiki, then repositoryId is required
return data.type !== WikiType.CodeWiki || !!data.repositoryId;
},
{
message: 'repositoryId is required when type is codeWiki',
path: ['repositoryId'],
},
);
```
--------------------------------------------------------------------------------
/src/features/wikis/get-wiki-page/feature.ts:
--------------------------------------------------------------------------------
```typescript
import * as azureDevOpsClient from '../../../clients/azure-devops';
import { AzureDevOpsError } from '../../../shared/errors/azure-devops-errors';
/**
* Options for getting a wiki page
*/
export interface GetWikiPageOptions {
/**
* The ID or name of the organization
* If not provided, the default organization will be used
*/
organizationId: string;
/**
* The ID or name of the project
* If not provided, the default project will be used
*/
projectId: string;
/**
* The ID or name of the wiki
*/
wikiId: string;
/**
* The path of the page within the wiki
*/
pagePath: string;
}
/**
* Get a wiki page from a wiki
*
* @param options Options for getting a wiki page
* @returns Wiki page content as text/plain
* @throws {AzureDevOpsResourceNotFoundError} When the wiki page is not found
* @throws {AzureDevOpsPermissionError} When the user does not have permission to access the wiki page
* @throws {AzureDevOpsError} When an error occurs while fetching the wiki page
*/
export async function getWikiPage(
options: GetWikiPageOptions,
): Promise<string> {
const { organizationId, projectId, wikiId, pagePath } = options;
try {
// Create the client
const client = await azureDevOpsClient.getWikiClient({
organizationId,
});
// Get the wiki page
return (await client.getPage(projectId, wikiId, pagePath)).content;
} catch (error) {
// If it's already an AzureDevOpsError, rethrow it
if (error instanceof AzureDevOpsError) {
throw error;
}
// Otherwise wrap it in an AzureDevOpsError
throw new AzureDevOpsError('Failed to get wiki page', { cause: error });
}
}
```
--------------------------------------------------------------------------------
/src/shared/test/test-helpers.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { getPersonalAccessTokenHandler } from 'azure-devops-node-api';
import { AzureDevOpsConfig } from '../types';
import { AuthenticationMethod } from '../auth';
/**
* Creates a WebApi connection for tests with real credentials
*
* @returns WebApi connection
*/
export async function getTestConnection(): Promise<WebApi | null> {
// If we have real credentials, use them
const orgUrl = process.env.AZURE_DEVOPS_ORG_URL;
const token = process.env.AZURE_DEVOPS_PAT;
if (orgUrl && token) {
const authHandler = getPersonalAccessTokenHandler(token);
return new WebApi(orgUrl, authHandler);
}
// If we don't have credentials, return null
return null;
}
/**
* Creates test configuration for Azure DevOps tests
*
* @returns Azure DevOps config
*/
export function getTestConfig(): AzureDevOpsConfig | null {
// If we have real credentials, use them
const orgUrl = process.env.AZURE_DEVOPS_ORG_URL;
const pat = process.env.AZURE_DEVOPS_PAT;
if (orgUrl && pat) {
return {
organizationUrl: orgUrl,
authMethod: AuthenticationMethod.PersonalAccessToken,
personalAccessToken: pat,
defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT,
};
}
// If we don't have credentials, return null
return null;
}
/**
* Determines if integration tests should be skipped
*
* @returns true if integration tests should be skipped
*/
export function shouldSkipIntegrationTest(): boolean {
if (!process.env.AZURE_DEVOPS_ORG_URL || !process.env.AZURE_DEVOPS_PAT) {
console.log(
'Skipping integration test: No real Azure DevOps connection available',
);
return true;
}
return false;
}
```
--------------------------------------------------------------------------------
/src/features/repositories/list-commits/feature.spec.unit.ts:
--------------------------------------------------------------------------------
```typescript
import { Readable } from 'stream';
import { listCommits } from './feature';
describe('listCommits', () => {
it('should return commits with file patches', async () => {
const mockCommits = [
{
commitId: '123',
comment: 'Initial commit',
author: { name: 'Author' },
committer: { name: 'Committer' },
url: 'https://example.com',
parents: ['abc'],
},
];
const getCommits = jest.fn().mockResolvedValue(mockCommits);
const getChanges = jest.fn().mockResolvedValue({
changes: [
{
item: {
path: '/file.ts',
objectId: 'new',
originalObjectId: 'old',
},
originalPath: '/file.ts',
},
],
});
const getBlobContent = jest
.fn()
.mockImplementation((_repo: string, objectId: string) => {
const content =
objectId === 'old'
? 'console.log("old");\n'
: 'console.log("new");\n';
return Readable.from([content]);
});
const mockConnection: any = {
getGitApi: jest.fn().mockResolvedValue({
getCommits,
getChanges,
getBlobContent,
}),
};
const result = await listCommits(mockConnection, {
projectId: 'p',
repositoryId: 'r',
branchName: 'main',
});
expect(result.commits).toHaveLength(1);
expect(result.commits[0].files).toHaveLength(1);
expect(result.commits[0].files[0].patch).toContain('-console.log("old");');
expect(result.commits[0].files[0].patch).toContain('+console.log("new");');
expect(getCommits).toHaveBeenCalled();
expect(getChanges).toHaveBeenCalledWith('123', 'r', 'p');
});
});
```
--------------------------------------------------------------------------------
/docs/testing/setup.md:
--------------------------------------------------------------------------------
```markdown
# Testing Setup Guide
## Environment Variables
Tests that interact with Azure DevOps APIs (integration and e2e tests) require environment variables to run properly. These variables are automatically loaded from your `.env` file during test execution.
Required variables:
```
AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization
AZURE_DEVOPS_PAT=your-personal-access-token
AZURE_DEVOPS_DEFAULT_PROJECT=your-project-name
```
## Test Structure
Tests in this project are co-located with the code they're testing:
```
src/
features/
feature-name/
feature.ts
feature.spec.unit.ts # Unit tests
feature.spec.int.ts # Integration tests
```
E2E tests are only located at the server level:
```
src/
server.ts
server.spec.e2e.ts # E2E tests
```
## Import Pattern
We use path aliases to make imports cleaner and easier to maintain. Instead of relative imports like:
```typescript
import { someFunction } from '../../../../shared/utils';
```
You can use the `@/` path alias:
```typescript
import { someFunction } from '@/shared/utils';
```
### Test Helpers
Test helpers are located in a centralized location for all tests:
```typescript
import { getTestConnection, shouldSkipIntegrationTest } from '@/shared/test/test-helpers';
```
## Running Tests
- Unit tests: `npm run test:unit`
- Integration tests: `npm run test:int`
- E2E tests: `npm run test:e2e`
- All tests: `npm test`
## VSCode Integration
The project includes VSCode settings that:
1. Show proper test icons for `*.spec.*.ts` files
2. Enable file nesting to group test files with their implementation
3. Configure TypeScript to prefer path aliases over relative imports
These settings are stored in `.vscode/settings.json`.
```
--------------------------------------------------------------------------------
/src/features/work-items/update-work-item/feature.spec.unit.ts:
--------------------------------------------------------------------------------
```typescript
import { updateWorkItem } from './feature';
import { AzureDevOpsError } from '../../../shared/errors';
// Unit tests should only focus on isolated logic
// No real connections, HTTP requests, or dependencies
describe('updateWorkItem unit', () => {
test('should throw error when no fields are provided for update', async () => {
// Arrange - mock connection, never used due to validation error
const mockConnection: any = {
getWorkItemTrackingApi: jest.fn(),
};
// Act & Assert - empty options object should throw
await expect(
updateWorkItem(
mockConnection,
123,
{}, // No fields to update
),
).rejects.toThrow('At least one field must be provided for update');
});
test('should propagate custom errors when thrown internally', async () => {
// Arrange
const mockConnection: any = {
getWorkItemTrackingApi: jest.fn().mockImplementation(() => {
throw new AzureDevOpsError('Custom error');
}),
};
// Act & Assert
await expect(
updateWorkItem(mockConnection, 123, { title: 'Updated Title' }),
).rejects.toThrow(AzureDevOpsError);
await expect(
updateWorkItem(mockConnection, 123, { title: 'Updated Title' }),
).rejects.toThrow('Custom error');
});
test('should wrap unexpected errors in a friendly error message', async () => {
// Arrange
const mockConnection: any = {
getWorkItemTrackingApi: jest.fn().mockImplementation(() => {
throw new Error('Unexpected error');
}),
};
// Act & Assert
await expect(
updateWorkItem(mockConnection, 123, { title: 'Updated Title' }),
).rejects.toThrow('Failed to update work item: Unexpected error');
});
});
```
--------------------------------------------------------------------------------
/src/features/wikis/create-wiki/feature.spec.int.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { createWiki } from './feature';
import { WikiType } from './schema';
import { getTestConnection } from '@/shared/test/test-helpers';
import axios from 'axios';
axios.interceptors.request.use((request) => {
console.log('Starting Request', JSON.stringify(request, null, 2));
return request;
});
describe('createWiki (Integration)', () => {
let connection: WebApi | null = null;
let projectName: string;
const testWikiName = `TestWiki_${new Date().getTime()}`;
beforeAll(async () => {
// Get a real connection using environment variables
connection = await getTestConnection();
projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
});
test.skip('should create a project wiki', async () => {
// PERMANENTLY SKIPPED: Azure DevOps only allows one wiki per project.
// Running this test multiple times would fail after the first wiki is created.
// This test is kept for reference but cannot be run repeatedly.
// 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 the wiki
const wiki = await createWiki(connection, {
name: testWikiName,
projectId: projectName,
type: WikiType.ProjectWiki,
});
// Verify the wiki was created
expect(wiki).toBeDefined();
expect(wiki.name).toBe(testWikiName);
expect(wiki.projectId).toBe(projectName);
expect(wiki.type).toBe(WikiType.ProjectWiki);
});
// NOTE: We're not testing code wiki creation since that requires a repository
// that would need to be created/cleaned up and is outside the scope of this test
});
```
--------------------------------------------------------------------------------
/src/features/wikis/list-wiki-pages/feature.ts:
--------------------------------------------------------------------------------
```typescript
import * as azureDevOpsClient from '../../../clients/azure-devops';
import { AzureDevOpsError } from '../../../shared/errors/azure-devops-errors';
import { defaultOrg, defaultProject } from '../../../utils/environment';
import { ListWikiPagesOptions } from './schema';
/**
* Summary information for a wiki page
*/
export interface WikiPageSummary {
id: number;
path: string;
url?: string;
order?: number;
}
/**
* List wiki pages from a wiki
*
* @param options Options for listing wiki pages
* @returns Array of wiki page summaries
* @throws {AzureDevOpsResourceNotFoundError} When the wiki is not found
* @throws {AzureDevOpsPermissionError} When the user does not have permission to access the wiki
* @throws {AzureDevOpsError} When an error occurs while fetching the wiki pages
*/
export async function listWikiPages(
options: ListWikiPagesOptions,
): Promise<WikiPageSummary[]> {
const { organizationId, projectId, wikiId } = options;
// Use defaults if not provided
const orgId = organizationId || defaultOrg;
const projId = projectId || defaultProject;
try {
// Create the client
const client = await azureDevOpsClient.getWikiClient({
organizationId: orgId,
});
// Get the wiki pages
const pages = await client.listWikiPages(projId, wikiId);
// Return the pages directly since the client interface now matches our requirements
return pages.map((page) => ({
id: page.id,
path: page.path,
url: page.url,
order: page.order,
}));
} catch (error) {
// If it's already an AzureDevOpsError, rethrow it
if (error instanceof AzureDevOpsError) {
throw error;
}
// Otherwise wrap it in an AzureDevOpsError
throw new AzureDevOpsError('Failed to list wiki pages', { cause: error });
}
}
```
--------------------------------------------------------------------------------
/.github/skills/skill-creator/references/output-patterns.md:
--------------------------------------------------------------------------------
```markdown
# Output Patterns
Use these patterns when skills need to produce consistent, high-quality output.
## Template Pattern
Provide templates for output format. Match the level of strictness to your needs.
**For strict requirements (like API responses or data formats):**
```markdown
## Report structure
ALWAYS use this exact template structure:
# [Analysis Title]
## Executive summary
[One-paragraph overview of key findings]
## Key findings
- Finding 1 with supporting data
- Finding 2 with supporting data
- Finding 3 with supporting data
## Recommendations
1. Specific actionable recommendation
2. Specific actionable recommendation
```
**For flexible guidance (when adaptation is useful):**
```markdown
## Report structure
Here is a sensible default format, but use your best judgment:
# [Analysis Title]
## Executive summary
[Overview]
## Key findings
[Adapt sections based on what you discover]
## Recommendations
[Tailor to the specific context]
Adjust sections as needed for the specific analysis type.
```
## Examples Pattern
For skills where output quality depends on seeing examples, provide input/output pairs:
```markdown
## Commit message format
Generate commit messages following these examples:
**Example 1:**
Input: Added user authentication with JWT tokens
Output:
```
feat(auth): implement JWT-based authentication
Add login endpoint and token validation middleware
```
**Example 2:**
Input: Fixed bug where dates displayed incorrectly in reports
Output:
```
fix(reports): correct date formatting in timezone conversion
Use UTC timestamps consistently across report generation
```
Follow this style: type(scope): brief description, then detailed explanation.
```
Examples help Claude understand the desired style and level of detail more clearly than descriptions alone.
```
--------------------------------------------------------------------------------
/src/features/wikis/update-wiki-page/feature.spec.int.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { updateWikiPage } from './feature';
import {
getTestConnection,
shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';
describe('updateWikiPage integration', () => {
let connection: WebApi | null = null;
let projectName: string;
let organizationName: string;
let wikiId: string;
beforeAll(async () => {
// Get a real connection using environment variables
connection = await getTestConnection();
projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
organizationName = process.env.AZURE_DEVOPS_ORGANIZATION || '';
// Note: You'll need to set this to a valid wiki ID in your environment
wikiId = `${projectName}.wiki`;
});
test('should update a wiki page 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',
);
}
// Skip if no wiki ID is provided
if (!wikiId) {
console.log('Skipping test: No wiki ID provided');
return;
}
const testPagePath = '/test-page';
const testContent = '# Test Content\nThis is a test update.';
const testComment = 'Test update from integration test';
// Update the wiki page
const result = await updateWikiPage({
organizationId: organizationName,
projectId: projectName,
wikiId: wikiId,
pagePath: testPagePath,
content: testContent,
comment: testComment,
});
// Verify the result
expect(result).toBeDefined();
expect(result.path).toBe(testPagePath);
expect(result.content).toBe(testContent);
});
});
```
--------------------------------------------------------------------------------
/tests/setup.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Jest setup file that runs before all tests
*/
import dotenv from 'dotenv';
import path from 'path';
// Load environment variables from .env file
// Use silent mode to prevent warning when .env file is not found
const result = dotenv.config({
path: path.resolve(process.cwd(), '.env'),
});
// Only log if .env file was successfully loaded and DEBUG=true
if (!result.error && process.env.DEBUG === 'true') {
console.log('Environment variables loaded from .env file');
}
// Increase timeout for integration tests
jest.setTimeout(30000); // 30 seconds
// Suppress console output during tests unless specifically desired
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
const originalConsoleError = console.error;
if (process.env.DEBUG !== 'true') {
global.console.log = (...args: any[]) => {
if (
args[0]?.toString().includes('Skip') ||
args[0]?.toString().includes('Environment')
) {
originalConsoleLog(...args);
}
};
global.console.warn = (...args: any[]) => {
if (args[0]?.toString().includes('Warning')) {
originalConsoleWarn(...args);
}
};
global.console.error = (...args: any[]) => {
originalConsoleError(...args);
};
}
// Global setup before tests run
beforeAll(() => {
console.log('Starting tests with Testing Trophy approach...');
});
// Global cleanup after all tests
afterAll(() => {
console.log('All tests completed.');
});
// Clear all mocks before each test
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(console, 'log').mockImplementation(originalConsoleLog);
jest.spyOn(console, 'warn').mockImplementation(originalConsoleWarn);
jest.spyOn(console, 'error').mockImplementation(originalConsoleError);
});
// Restore all mocks after each test
afterEach(() => {
jest.restoreAllMocks();
});
```
--------------------------------------------------------------------------------
/src/features/work-items/create-work-item/feature.spec.unit.ts:
--------------------------------------------------------------------------------
```typescript
import { createWorkItem } from './feature';
import { AzureDevOpsError } from '../../../shared/errors';
// Unit tests should only focus on isolated logic
// No real connections, HTTP requests, or dependencies
describe('createWorkItem unit', () => {
// Test for required title validation
test('should throw error when title is not provided', async () => {
// Arrange - mock connection, never used due to validation error
const mockConnection: any = {
getWorkItemTrackingApi: jest.fn(),
};
// Act & Assert
await expect(
createWorkItem(
mockConnection,
'TestProject',
'Task',
{ title: '' }, // Empty title
),
).rejects.toThrow('Title is required');
});
// Test for error propagation
test('should propagate custom errors when thrown internally', async () => {
// Arrange
const mockConnection: any = {
getWorkItemTrackingApi: jest.fn().mockImplementation(() => {
throw new AzureDevOpsError('Custom error');
}),
};
// Act & Assert
await expect(
createWorkItem(mockConnection, 'TestProject', 'Task', {
title: 'Test Task',
}),
).rejects.toThrow(AzureDevOpsError);
await expect(
createWorkItem(mockConnection, 'TestProject', 'Task', {
title: 'Test Task',
}),
).rejects.toThrow('Custom error');
});
test('should wrap unexpected errors in a friendly error message', async () => {
// Arrange
const mockConnection: any = {
getWorkItemTrackingApi: jest.fn().mockImplementation(() => {
throw new Error('Unexpected error');
}),
};
// Act & Assert
await expect(
createWorkItem(mockConnection, 'TestProject', 'Task', {
title: 'Test Task',
}),
).rejects.toThrow('Failed to create work item: Unexpected error');
});
});
```
--------------------------------------------------------------------------------
/src/features/wikis/get-wikis/feature.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { WikiV2 } from 'azure-devops-node-api/interfaces/WikiInterfaces';
import {
AzureDevOpsError,
AzureDevOpsResourceNotFoundError,
} from '../../../shared/errors';
/**
* Options for getting wikis
*/
export interface GetWikisOptions {
/**
* The ID or name of the organization
* If not provided, the default organization will be used
*/
organizationId?: string;
/**
* The ID or name of the project
* If not provided, the wikis from all projects will be returned
*/
projectId?: string;
}
/**
* Get wikis in a project or organization
*
* @param connection The Azure DevOps WebApi connection
* @param options Options for getting wikis
* @returns List of wikis
*/
export async function getWikis(
connection: WebApi,
options: GetWikisOptions,
): Promise<WikiV2[]> {
try {
// Get the Wiki API client
const wikiApi = await connection.getWikiApi();
// If a projectId is provided, get wikis for that specific project
// Otherwise, get wikis for the entire organization
const { projectId } = options;
const wikis = await wikiApi.getAllWikis(projectId);
return wikis || [];
} catch (error) {
// Handle resource not found errors specifically
if (
error instanceof Error &&
error.message &&
error.message.includes('The resource cannot be found')
) {
throw new AzureDevOpsResourceNotFoundError(
`Resource not found: ${options.projectId ? `Project '${options.projectId}'` : 'Organization'}`,
);
}
// 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 wikis: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
```
--------------------------------------------------------------------------------
/src/features/pipelines/list-pipelines/feature.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import {
AzureDevOpsError,
AzureDevOpsAuthenticationError,
AzureDevOpsResourceNotFoundError,
} from '../../../shared/errors';
import { ListPipelinesOptions, Pipeline } from '../types';
/**
* List pipelines in a project
*
* @param connection The Azure DevOps WebApi connection
* @param options Options for listing pipelines
* @returns List of pipelines
*/
export async function listPipelines(
connection: WebApi,
options: ListPipelinesOptions,
): Promise<Pipeline[]> {
try {
const pipelinesApi = await connection.getPipelinesApi();
const { projectId, orderBy, top, continuationToken } = options;
// Call the pipelines API to get the list of pipelines
const pipelines = await pipelinesApi.listPipelines(
projectId,
orderBy,
top,
continuationToken,
);
return pipelines;
} catch (error) {
// Handle specific error types
if (error instanceof AzureDevOpsError) {
throw error;
}
// Check for specific error types and convert to appropriate Azure DevOps errors
if (error instanceof Error) {
if (
error.message.includes('Authentication') ||
error.message.includes('Unauthorized') ||
error.message.includes('401')
) {
throw new AzureDevOpsAuthenticationError(
`Failed to authenticate: ${error.message}`,
);
}
if (
error.message.includes('not found') ||
error.message.includes('does not exist') ||
error.message.includes('404')
) {
throw new AzureDevOpsResourceNotFoundError(
`Project or resource not found: ${error.message}`,
);
}
}
// Otherwise, wrap it in a generic error
throw new AzureDevOpsError(
`Failed to list pipelines: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
```
--------------------------------------------------------------------------------
/src/features/repositories/get-repository/feature.spec.unit.ts:
--------------------------------------------------------------------------------
```typescript
import { getRepository } from './feature';
import {
AzureDevOpsError,
AzureDevOpsResourceNotFoundError,
} from '../../../shared/errors';
// Unit tests should only focus on isolated logic
// No real connections, HTTP requests, or dependencies
describe('getRepository unit', () => {
test('should propagate resource not found errors', async () => {
// Arrange
const mockConnection: any = {
getGitApi: jest.fn().mockImplementation(() => ({
getRepository: jest.fn().mockResolvedValue(null), // Simulate repository not found
})),
};
// Act & Assert
await expect(
getRepository(mockConnection, 'test-project', 'non-existent-repo'),
).rejects.toThrow(AzureDevOpsResourceNotFoundError);
await expect(
getRepository(mockConnection, 'test-project', 'non-existent-repo'),
).rejects.toThrow(
"Repository 'non-existent-repo' not found in project 'test-project'",
);
});
test('should propagate custom errors when thrown internally', async () => {
// Arrange
const mockConnection: any = {
getGitApi: jest.fn().mockImplementation(() => {
throw new AzureDevOpsError('Custom error');
}),
};
// Act & Assert
await expect(
getRepository(mockConnection, 'test-project', 'test-repo'),
).rejects.toThrow(AzureDevOpsError);
await expect(
getRepository(mockConnection, 'test-project', 'test-repo'),
).rejects.toThrow('Custom error');
});
test('should wrap unexpected errors in a friendly error message', async () => {
// Arrange
const mockConnection: any = {
getGitApi: jest.fn().mockImplementation(() => {
throw new Error('Unexpected error');
}),
};
// Act & Assert
await expect(
getRepository(mockConnection, 'test-project', 'test-repo'),
).rejects.toThrow('Failed to get repository: Unexpected error');
});
});
```
--------------------------------------------------------------------------------
/docs/ci-setup.md:
--------------------------------------------------------------------------------
```markdown
# CI Environment Setup for Integration Tests
This document explains how to set up the CI environment to run integration tests with Azure DevOps.
## GitHub Secrets Configuration
To run integration tests in the CI environment, you need to configure the following GitHub Secrets:
1. **AZURE_DEVOPS_ORG_URL**: The URL of your Azure DevOps organization (e.g., `https://dev.azure.com/your-organization`)
2. **AZURE_DEVOPS_PAT**: A Personal Access Token with appropriate permissions
3. **AZURE_DEVOPS_DEFAULT_PROJECT** (optional): The default project to use for tests
### Setting up GitHub Secrets
1. Go to your GitHub repository
2. Click on "Settings" > "Secrets and variables" > "Actions"
3. Click on "New repository secret"
4. Add each of the required secrets:
#### AZURE_DEVOPS_ORG_URL
- Name: `AZURE_DEVOPS_ORG_URL`
- Value: `https://dev.azure.com/your-organization`
#### AZURE_DEVOPS_PAT
- Name: `AZURE_DEVOPS_PAT`
- Value: Your Personal Access Token
#### AZURE_DEVOPS_DEFAULT_PROJECT (optional)
- Name: `AZURE_DEVOPS_DEFAULT_PROJECT`
- Value: Your project name
## Personal Access Token (PAT) Requirements
The PAT used for integration tests should have the following permissions:
- **Code**: Read & Write
- **Work Items**: Read & Write
- **Build**: Read & Execute
- **Project and Team**: Read
- **Graph**: Read
- **Release**: Read & Execute
## Security Considerations
- Use a dedicated Azure DevOps organization or project for testing
- Create a PAT with the minimum required permissions
- Consider setting an expiration date for the PAT
- Regularly rotate the PAT used in GitHub Secrets
## Troubleshooting
If integration tests fail in CI:
1. Check the GitHub Actions logs for detailed error messages
2. Verify that the PAT has not expired
3. Ensure the PAT has the required permissions
4. Confirm that the organization URL is correct
5. Check if the default project exists and is accessible with the provided PAT
```
--------------------------------------------------------------------------------
/src/features/search/index.ts:
--------------------------------------------------------------------------------
```typescript
export * from './schemas';
export * from './types';
export * from './search-code';
export * from './search-wiki';
export * from './search-work-items';
// 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 {
SearchCodeSchema,
SearchWikiSchema,
SearchWorkItemsSchema,
searchCode,
searchWiki,
searchWorkItems,
} from './';
/**
* Checks if the request is for the search feature
*/
export const isSearchRequest: RequestIdentifier = (
request: CallToolRequest,
): boolean => {
const toolName = request.params.name;
return ['search_code', 'search_wiki', 'search_work_items'].includes(toolName);
};
/**
* Handles search feature requests
*/
export const handleSearchRequest: RequestHandler = async (
connection: WebApi,
request: CallToolRequest,
): Promise<{ content: Array<{ type: string; text: string }> }> => {
switch (request.params.name) {
case 'search_code': {
const args = SearchCodeSchema.parse(request.params.arguments);
const result = await searchCode(connection, args);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
}
case 'search_wiki': {
const args = SearchWikiSchema.parse(request.params.arguments);
const result = await searchWiki(connection, args);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
}
case 'search_work_items': {
const args = SearchWorkItemsSchema.parse(request.params.arguments);
const result = await searchWorkItems(connection, args);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
}
default:
throw new Error(`Unknown search tool: ${request.params.name}`);
}
};
```
--------------------------------------------------------------------------------
/src/features/projects/get-project/feature.spec.int.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { getProject } from './feature';
import {
getTestConnection,
shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';
describe('getProject integration', () => {
let connection: WebApi | null = null;
let projectName: string;
beforeAll(async () => {
// Get a real connection using environment variables
connection = await getTestConnection();
projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
});
test('should retrieve a real project from Azure DevOps', async () => {
// Skip if no connection is available
if (shouldSkipIntegrationTest()) {
return;
}
// This connection must be available if we didn't skip
if (!connection) {
throw new Error(
'Connection should be available when test is not skipped',
);
}
// Act - make an actual API call to Azure DevOps
const result = await getProject(connection, projectName);
// Assert on the actual response
expect(result).toBeDefined();
expect(result.name).toBe(projectName);
expect(result.id).toBeDefined();
expect(result.url).toBeDefined();
expect(result.state).toBeDefined();
// Verify basic project structure
expect(result.visibility).toBeDefined();
expect(result.lastUpdateTime).toBeDefined();
});
test('should throw error when project is not found', async () => {
// Skip if no connection is available
if (shouldSkipIntegrationTest()) {
return;
}
// This connection must be available if we didn't skip
if (!connection) {
throw new Error(
'Connection should be available when test is not skipped',
);
}
// Use a non-existent project name
const nonExistentProjectName = 'non-existent-project-' + Date.now();
// Act & Assert - should throw an error for non-existent project
await expect(
getProject(connection, nonExistentProjectName),
).rejects.toThrow(/not found|Failed to get project/);
});
});
```
--------------------------------------------------------------------------------
/src/features/organizations/index.ts:
--------------------------------------------------------------------------------
```typescript
// Re-export schemas and types
export * from './schemas';
export * from './types';
// Re-export features
export * from './list-organizations';
// 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 { listOrganizations } from './list-organizations';
import { AzureDevOpsConfig } from '../../shared/types';
import { AuthenticationMethod } from '../../shared/auth';
/**
* Checks if the request is for the organizations feature
*/
export const isOrganizationsRequest: RequestIdentifier = (
request: CallToolRequest,
): boolean => {
const toolName = request.params.name;
return ['list_organizations'].includes(toolName);
};
/**
* Handles organizations feature requests
*/
export const handleOrganizationsRequest: RequestHandler = async (
connection: WebApi,
request: CallToolRequest,
): Promise<{ content: Array<{ type: string; text: string }> }> => {
switch (request.params.name) {
case 'list_organizations': {
// Use environment variables for authentication method and PAT
// This matches how other features handle authentication
const config: AzureDevOpsConfig = {
authMethod:
process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'pat'
? AuthenticationMethod.PersonalAccessToken
: process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() ===
'azure-cli'
? AuthenticationMethod.AzureCli
: AuthenticationMethod.AzureIdentity,
personalAccessToken: process.env.AZURE_DEVOPS_PAT,
organizationUrl: connection.serverUrl || '',
};
const result = await listOrganizations(config);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
}
default:
throw new Error(`Unknown organizations tool: ${request.params.name}`);
}
};
```
--------------------------------------------------------------------------------
/src/features/pipelines/get-pipeline/feature.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import {
AzureDevOpsError,
AzureDevOpsAuthenticationError,
AzureDevOpsResourceNotFoundError,
} from '../../../shared/errors';
import { GetPipelineOptions, Pipeline } from '../types';
/**
* Get a specific pipeline by ID
*
* @param connection The Azure DevOps WebApi connection
* @param options Options for getting a pipeline
* @returns Pipeline details
*/
export async function getPipeline(
connection: WebApi,
options: GetPipelineOptions,
): Promise<Pipeline> {
try {
const pipelinesApi = await connection.getPipelinesApi();
const { projectId, pipelineId, pipelineVersion } = options;
// Call the pipelines API to get the pipeline
const pipeline = await pipelinesApi.getPipeline(
projectId,
pipelineId,
pipelineVersion,
);
// If pipeline not found, API returns null instead of throwing error
if (pipeline === null) {
throw new AzureDevOpsResourceNotFoundError(
`Pipeline not found with ID: ${pipelineId}`,
);
}
return pipeline;
} catch (error) {
// Handle specific error types
if (error instanceof AzureDevOpsError) {
throw error;
}
// Check for specific error types and convert to appropriate Azure DevOps errors
if (error instanceof Error) {
if (
error.message.includes('Authentication') ||
error.message.includes('Unauthorized') ||
error.message.includes('401')
) {
throw new AzureDevOpsAuthenticationError(
`Failed to authenticate: ${error.message}`,
);
}
if (
error.message.includes('not found') ||
error.message.includes('does not exist') ||
error.message.includes('404')
) {
throw new AzureDevOpsResourceNotFoundError(
`Pipeline or project not found: ${error.message}`,
);
}
}
// Otherwise, wrap it in a generic error
throw new AzureDevOpsError(
`Failed to get pipeline: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
```
--------------------------------------------------------------------------------
/docs/tools/organizations.md:
--------------------------------------------------------------------------------
```markdown
# Azure DevOps Organizations Tools
This document describes the tools available for working with Azure DevOps organizations.
## list_organizations
Lists all Azure DevOps organizations accessible to the authenticated user.
### Description
The `list_organizations` tool retrieves all Azure DevOps organizations that the authenticated user has access to. This is useful for discovering which organizations are available before performing operations on specific projects or repositories.
Unlike most other tools in this server, this tool uses Axios for direct API calls rather than the Azure DevOps Node API client, as the WebApi client doesn't support the organizations endpoint.
### Parameters
This tool doesn't require any parameters.
```json
{
// No parameters required
}
```
### Response
The tool returns an array of organization objects, each containing:
- `id`: The unique identifier of the organization
- `name`: The name of the organization
- `url`: The URL of the organization
Example response:
```json
[
{
"id": "org1-id",
"name": "org1-name",
"url": "https://dev.azure.com/org1-name"
},
{
"id": "org2-id",
"name": "org2-name",
"url": "https://dev.azure.com/org2-name"
}
]
```
### Error Handling
The tool may throw the following errors:
- `AzureDevOpsAuthenticationError`: If authentication fails or the user profile cannot be retrieved
- General errors: If the accounts API call fails or other unexpected errors occur
### Example Usage
```typescript
// Example MCP client call
const result = await mcpClient.callTool('list_organizations', {});
console.log(result);
```
### Implementation Details
This tool uses a two-step process to retrieve organizations:
1. First, it gets the user profile from `https://app.vssps.visualstudio.com/_apis/profile/profiles/me`
2. Then it extracts the `publicAlias` from the profile response
3. Finally, it uses the `publicAlias` to get organizations from `https://app.vssps.visualstudio.com/_apis/accounts?memberId={publicAlias}`
Authentication is handled using Basic Auth with the Personal Access Token.
```
--------------------------------------------------------------------------------
/src/features/projects/schemas.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from 'zod';
import { defaultProject, defaultOrg } from '../../utils/environment';
/**
* Schema for getting a project
*/
export const GetProjectSchema = 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})`),
});
/**
* Schema for getting detailed project information
*/
export const GetProjectDetailsSchema = 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})`),
includeProcess: z
.boolean()
.optional()
.default(false)
.describe('Include process information in the project result'),
includeWorkItemTypes: z
.boolean()
.optional()
.default(false)
.describe('Include work item types and their structure'),
includeFields: z
.boolean()
.optional()
.default(false)
.describe('Include field information for work item types'),
includeTeams: z
.boolean()
.optional()
.default(false)
.describe('Include associated teams in the project result'),
expandTeamIdentity: z
.boolean()
.optional()
.default(false)
.describe('Expand identity information in the team objects'),
});
/**
* Schema for listing projects
*/
export const ListProjectsSchema = z.object({
organizationId: z
.string()
.optional()
.describe(`The ID or name of the organization (Default: ${defaultOrg})`),
stateFilter: z
.number()
.optional()
.describe(
'Filter on team project state (0: all, 1: well-formed, 2: creating, 3: deleting, 4: new)',
),
top: z.number().optional().describe('Maximum number of projects to return'),
skip: z.number().optional().describe('Number of projects to skip'),
continuationToken: z
.number()
.optional()
.describe('Gets the projects after the continuation token provided'),
});
```
--------------------------------------------------------------------------------
/src/features/repositories/list-repositories/feature.spec.unit.ts:
--------------------------------------------------------------------------------
```typescript
import { listRepositories } from './feature';
import { AzureDevOpsError } from '../../../shared/errors';
// Unit tests should only focus on isolated logic
describe('listRepositories unit', () => {
test('should return empty array when no repositories are found', async () => {
// Arrange
const mockConnection: any = {
getGitApi: jest.fn().mockImplementation(() => ({
getRepositories: jest.fn().mockResolvedValue([]), // No repositories found
})),
};
// Act
const result = await listRepositories(mockConnection, {
projectId: 'test-project',
});
// Assert
expect(result).toEqual([]);
});
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(
listRepositories(mockConnection, { projectId: 'test-project' }),
).rejects.toThrow(AzureDevOpsError);
await expect(
listRepositories(mockConnection, { projectId: 'test-project' }),
).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(
listRepositories(mockConnection, { projectId: 'test-project' }),
).rejects.toThrow('Failed to list repositories: Unexpected error');
});
test('should respect the includeLinks option', async () => {
// Arrange
const mockGetRepositories = jest.fn().mockResolvedValue([]);
const mockConnection: any = {
getGitApi: jest.fn().mockImplementation(() => ({
getRepositories: mockGetRepositories,
})),
};
// Act
await listRepositories(mockConnection, {
projectId: 'test-project',
includeLinks: true,
});
// Assert
expect(mockGetRepositories).toHaveBeenCalledWith('test-project', true);
});
});
```
--------------------------------------------------------------------------------
/src/features/pull-requests/tool-definitions.ts:
--------------------------------------------------------------------------------
```typescript
import { zodToJsonSchema } from 'zod-to-json-schema';
import { ToolDefinition } from '../../shared/types/tool-definition';
import {
CreatePullRequestSchema,
ListPullRequestsSchema,
GetPullRequestCommentsSchema,
AddPullRequestCommentSchema,
UpdatePullRequestSchema,
GetPullRequestChangesSchema,
GetPullRequestChecksSchema,
} from './schemas';
/**
* List of pull requests tools
*/
export const pullRequestsTools: ToolDefinition[] = [
{
name: 'create_pull_request',
description:
'Create a new pull request, including reviewers, linked work items, and optional tags',
inputSchema: zodToJsonSchema(CreatePullRequestSchema),
},
{
name: 'list_pull_requests',
description: 'List pull requests in a repository',
inputSchema: zodToJsonSchema(ListPullRequestsSchema),
},
{
name: 'get_pull_request_comments',
description: 'Get comments from a specific pull request',
inputSchema: zodToJsonSchema(GetPullRequestCommentsSchema),
},
{
name: 'add_pull_request_comment',
description:
'Add a comment to a pull request (reply to existing comments or create new threads)',
inputSchema: zodToJsonSchema(AddPullRequestCommentSchema),
},
{
name: 'update_pull_request',
description:
'Update an existing pull request with new properties, manage reviewers and work items, and add or remove tags',
inputSchema: zodToJsonSchema(UpdatePullRequestSchema),
},
{
name: 'get_pull_request_changes',
description:
'Get the files changed in a pull request, their unified diffs, source/target branch names, and the status of policy evaluations',
inputSchema: zodToJsonSchema(GetPullRequestChangesSchema),
},
{
name: 'get_pull_request_checks',
description: [
'Summarize the latest status checks and policy evaluations for a pull request.',
'- Surfaces pipeline and run identifiers so you can jump straight to the blocking validation.',
'- Pair with pipeline tools (e.g., get_pipeline_run, pipeline_timeline) to inspect failures in depth.',
].join('\n'),
inputSchema: zodToJsonSchema(GetPullRequestChecksSchema),
},
];
```
--------------------------------------------------------------------------------
/src/utils/environment.spec.unit.ts:
--------------------------------------------------------------------------------
```typescript
// Mock the environment module before importing
jest.mock('./environment', () => {
const original = jest.requireActual('./environment');
return {
...original,
// We'll keep getOrgNameFromUrl as is for its own tests
getOrgNameFromUrl: original.getOrgNameFromUrl,
};
});
import { getOrgNameFromUrl } from './environment';
describe('environment utilities', () => {
// Store original environment variables
const originalEnv = { ...process.env };
// Reset environment variables after each test
afterEach(() => {
process.env = { ...originalEnv };
jest.resetModules();
});
describe('getOrgNameFromUrl', () => {
it('should extract organization name from Azure DevOps URL', () => {
const url = 'https://dev.azure.com/test-organization';
expect(getOrgNameFromUrl(url)).toBe('test-organization');
});
it('should handle URLs with paths after the organization name', () => {
const url = 'https://dev.azure.com/test-organization/project';
expect(getOrgNameFromUrl(url)).toBe('test-organization');
});
it('should return "unknown-organization" when URL is undefined', () => {
expect(getOrgNameFromUrl(undefined)).toBe('unknown-organization');
});
it('should return "unknown-organization" when URL is empty', () => {
expect(getOrgNameFromUrl('')).toBe('unknown-organization');
});
it('should return "unknown-organization" when URL does not match pattern', () => {
const url = 'https://example.com/test-organization';
expect(getOrgNameFromUrl(url)).toBe('unknown-organization');
});
});
describe('defaultProject and defaultOrg', () => {
// Since we can't easily test the environment variable initialization directly,
// we'll test the getOrgNameFromUrl function which is used to derive defaultOrg
it('should handle the real default case', () => {
// This test is more of a documentation than a real test
const orgNameFromUrl = getOrgNameFromUrl(
process.env.AZURE_DEVOPS_ORG_URL,
);
// We can't assert an exact value since it depends on the environment
expect(typeof orgNameFromUrl).toBe('string');
});
});
});
```
--------------------------------------------------------------------------------
/src/features/repositories/get-repository-tree/feature.spec.unit.ts:
--------------------------------------------------------------------------------
```typescript
import { getRepositoryTree } from './feature';
import { AzureDevOpsError } from '../../../shared/errors';
describe('getRepositoryTree unit', () => {
test('should build tree structure from root', async () => {
const mockConnection: any = {
getGitApi: jest.fn().mockResolvedValue({
getRepository: jest.fn().mockResolvedValue({
id: '1',
name: 'test-repo',
defaultBranch: 'refs/heads/main',
}),
getItems: jest.fn().mockResolvedValue([
{ path: '/', isFolder: true },
{ path: '/README.md', isFolder: false },
{ path: '/src', isFolder: true },
{ path: '/src/index.ts', isFolder: false },
]),
}),
};
const result = await getRepositoryTree(mockConnection, {
projectId: 'proj',
repositoryId: '1',
});
expect(result.name).toBe('test-repo');
expect(result.stats.files).toBe(2);
expect(result.stats.directories).toBe(1);
expect(result.tree.length).toBe(3);
});
test('should respect depth when provided', async () => {
const mockConnection: any = {
getGitApi: jest.fn().mockResolvedValue({
getRepository: jest.fn().mockResolvedValue({
id: '1',
name: 'test-repo',
defaultBranch: 'refs/heads/main',
}),
getItems: jest.fn().mockResolvedValue([
{ path: '/src', isFolder: true },
{ path: '/src/index.ts', isFolder: false },
{ path: '/src/utils', isFolder: true },
{ path: '/src/utils/helper.ts', isFolder: false },
]),
}),
};
const result = await getRepositoryTree(mockConnection, {
projectId: 'proj',
repositoryId: '1',
path: '/src',
depth: 1,
});
expect(result.tree).toHaveLength(2);
expect(result.stats.files).toBe(1);
expect(result.stats.directories).toBe(1);
});
test('should throw AzureDevOpsError when repository not found', async () => {
const mockConnection: any = {
getGitApi: jest.fn().mockResolvedValue({
getRepository: jest.fn().mockResolvedValue(null),
}),
};
await expect(
getRepositoryTree(mockConnection, {
projectId: 'p',
repositoryId: 'missing',
}),
).rejects.toThrow(AzureDevOpsError);
});
});
```
--------------------------------------------------------------------------------
/src/features/pipelines/tool-definitions.ts:
--------------------------------------------------------------------------------
```typescript
import { zodToJsonSchema } from 'zod-to-json-schema';
import { ToolDefinition } from '../../shared/types/tool-definition';
import { ListPipelinesSchema } from './list-pipelines/schema';
import { GetPipelineSchema } from './get-pipeline/schema';
import { ListPipelineRunsSchema } from './list-pipeline-runs/schema';
import { GetPipelineRunSchema } from './get-pipeline-run/schema';
import { DownloadPipelineArtifactSchema } from './download-pipeline-artifact/schema';
import { GetPipelineTimelineSchema } from './pipeline-timeline/schema';
import { GetPipelineLogSchema } from './get-pipeline-log/schema';
import { TriggerPipelineSchema } from './trigger-pipeline/schema';
/**
* List of pipelines tools
*/
export const pipelinesTools: ToolDefinition[] = [
{
name: 'list_pipelines',
description: 'List pipelines in a project',
inputSchema: zodToJsonSchema(ListPipelinesSchema),
mcp_enabled: true,
},
{
name: 'get_pipeline',
description: 'Get details of a specific pipeline',
inputSchema: zodToJsonSchema(GetPipelineSchema),
mcp_enabled: true,
},
{
name: 'list_pipeline_runs',
description: 'List recent runs for a pipeline',
inputSchema: zodToJsonSchema(ListPipelineRunsSchema),
mcp_enabled: true,
},
{
name: 'get_pipeline_run',
description: 'Get details for a specific pipeline run',
inputSchema: zodToJsonSchema(GetPipelineRunSchema),
mcp_enabled: true,
},
{
name: 'download_pipeline_artifact',
description:
'Download a file from a pipeline run artifact and return its textual content',
inputSchema: zodToJsonSchema(DownloadPipelineArtifactSchema),
mcp_enabled: true,
},
{
name: 'pipeline_timeline',
description:
'Retrieve the timeline of stages and jobs for a pipeline run, to reduce the amount of data returned, you can filter by state and result',
inputSchema: zodToJsonSchema(GetPipelineTimelineSchema),
mcp_enabled: true,
},
{
name: 'get_pipeline_log',
description:
'Retrieve a specific pipeline log using the timeline log identifier',
inputSchema: zodToJsonSchema(GetPipelineLogSchema),
mcp_enabled: true,
},
{
name: 'trigger_pipeline',
description: 'Trigger a pipeline run',
inputSchema: zodToJsonSchema(TriggerPipelineSchema),
mcp_enabled: true,
},
];
```
--------------------------------------------------------------------------------
/src/features/wikis/get-wiki-page/feature.spec.int.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { getWikiPage } from './feature';
import { getWikis } from '../get-wikis/feature';
import {
getTestConnection,
shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';
import { getOrgNameFromUrl } from '@/utils/environment';
process.env.AZURE_DEVOPS_DEFAULT_PROJECT =
process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'default-project';
describe('getWikiPage integration', () => {
let connection: WebApi | null = null;
let projectName: string;
let orgUrl: string;
beforeAll(async () => {
// Mock the required environment variable for testing
process.env.AZURE_DEVOPS_ORG_URL =
process.env.AZURE_DEVOPS_ORG_URL || 'https://example.visualstudio.com';
// Get a real connection using environment variables
connection = await getTestConnection();
// Get and validate required environment variables
const envProjectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT;
if (!envProjectName) {
throw new Error(
'AZURE_DEVOPS_DEFAULT_PROJECT environment variable is required',
);
}
projectName = envProjectName;
const envOrgUrl = process.env.AZURE_DEVOPS_ORG_URL;
if (!envOrgUrl) {
throw new Error('AZURE_DEVOPS_ORG_URL environment variable is required');
}
orgUrl = envOrgUrl;
});
test('should retrieve a wiki page', async () => {
// Skip if no connection is available
if (shouldSkipIntegrationTest()) {
return;
}
// This connection must be available if we didn't skip
if (!connection) {
throw new Error(
'Connection should be available when test is not skipped',
);
}
// First get available wikis
const wikis = await getWikis(connection, { projectId: projectName });
// Skip if no wikis are available
if (wikis.length === 0) {
console.log('Skipping test: No wikis available in the project');
return;
}
// Use the first available wiki
const wiki = wikis[0];
if (!wiki.name) {
throw new Error('Wiki name is undefined');
}
// Get the wiki page
const result = await getWikiPage({
organizationId: getOrgNameFromUrl(orgUrl),
projectId: projectName,
wikiId: wiki.name,
pagePath: '/test',
});
// Verify the result
expect(result).toBeDefined();
expect(typeof result).toBe('string');
});
});
```
--------------------------------------------------------------------------------
/src/features/projects/get-project/feature.spec.unit.ts:
--------------------------------------------------------------------------------
```typescript
import { getProject } from './feature';
import {
AzureDevOpsError,
AzureDevOpsResourceNotFoundError,
} from '../../../shared/errors';
import { TeamProject } from 'azure-devops-node-api/interfaces/CoreInterfaces';
import { WebApi } from 'azure-devops-node-api';
// Create a partial mock interface for ICoreApi
interface MockCoreApi {
getProject: jest.Mock<Promise<TeamProject | null>>;
}
// Create a mock connection that resembles WebApi with minimal implementation
interface MockConnection {
getCoreApi: jest.Mock<Promise<MockCoreApi>>;
serverUrl?: string;
authHandler?: unknown;
rest?: unknown;
vsoClient?: unknown;
}
// Unit tests should only focus on isolated logic
describe('getProject unit', () => {
test('should throw resource not found error when project is null', async () => {
// Arrange
const mockCoreApi: MockCoreApi = {
getProject: jest.fn().mockResolvedValue(null), // Simulate project not found
};
const mockConnection: MockConnection = {
getCoreApi: jest.fn().mockResolvedValue(mockCoreApi),
};
// Act & Assert
await expect(
getProject(mockConnection as unknown as WebApi, 'non-existent-project'),
).rejects.toThrow(AzureDevOpsResourceNotFoundError);
await expect(
getProject(mockConnection as unknown as WebApi, 'non-existent-project'),
).rejects.toThrow("Project 'non-existent-project' not found");
});
test('should propagate custom errors when thrown internally', async () => {
// Arrange
const mockConnection: MockConnection = {
getCoreApi: jest.fn().mockImplementation(() => {
throw new AzureDevOpsError('Custom error');
}),
};
// Act & Assert
await expect(
getProject(mockConnection as unknown as WebApi, 'test-project'),
).rejects.toThrow(AzureDevOpsError);
await expect(
getProject(mockConnection as unknown as WebApi, 'test-project'),
).rejects.toThrow('Custom error');
});
test('should wrap unexpected errors in a friendly error message', async () => {
// Arrange
const mockConnection: MockConnection = {
getCoreApi: jest.fn().mockImplementation(() => {
throw new Error('Unexpected error');
}),
};
// Act & Assert
await expect(
getProject(mockConnection as unknown as WebApi, 'test-project'),
).rejects.toThrow('Failed to get project: Unexpected error');
});
});
```
--------------------------------------------------------------------------------
/finish_task.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/bash
# Check if a PR title is provided
if [ -z "$1" ]; then
echo "Usage: $0 <pr_title> [pr_description]"
echo "Example: $0 \"Add user authentication\" \"This PR implements user login and registration\""
exit 1
fi
PR_TITLE="$1"
PR_DESCRIPTION="${2:-"No description provided."}"
# Get current branch name
CURRENT_BRANCH=$(git symbolic-ref --short HEAD)
if [ "$CURRENT_BRANCH" = "main" ]; then
echo "Error: You are on the main branch. Please switch to a feature branch."
exit 1
fi
# Check if there are any uncommitted changes
if ! git diff --quiet || ! git diff --staged --quiet; then
# Stage all changes
echo "Staging all changes..."
git add .
# Commit changes
echo "Committing changes with title: $PR_TITLE"
git commit -m "$PR_TITLE" -m "$PR_DESCRIPTION"
if [ $? -ne 0 ]; then
echo "Failed to commit changes."
exit 1
fi
# Push changes to remote
echo "Pushing changes to origin/$CURRENT_BRANCH..."
git push -u origin "$CURRENT_BRANCH"
if [ $? -ne 0 ]; then
echo "Failed to push changes to remote."
exit 1
fi
else
echo "No uncommitted changes found. Proceeding with PR creation for already committed changes."
fi
# Create PR using GitHub CLI
echo "Creating pull request..."
if command -v gh &> /dev/null; then
PR_URL=$(gh pr create --title "$PR_TITLE" --body "$PR_DESCRIPTION" --base main --head "$CURRENT_BRANCH")
if [ $? -eq 0 ]; then
echo "Pull request created successfully!"
echo "PR URL: $PR_URL"
# Try to open the PR URL in the default browser
if command -v xdg-open &> /dev/null; then
xdg-open "$PR_URL" &> /dev/null & # Linux
elif command -v open &> /dev/null; then
open "$PR_URL" &> /dev/null & # macOS
elif command -v start &> /dev/null; then
start "$PR_URL" &> /dev/null & # Windows
else
echo "Could not automatically open the PR in your browser."
fi
else
echo "Failed to create pull request using GitHub CLI."
echo "Please create a PR manually at: https://github.com/$(git config --get remote.origin.url | sed 's/.*github.com[:\/]\(.*\)\.git/\1/')/pull/new/$CURRENT_BRANCH"
fi
else
echo "GitHub CLI (gh) not found. Please install it to create PRs from the command line."
echo "You can create a PR manually at: https://github.com/$(git config --get remote.origin.url | sed 's/.*github.com[:\/]\(.*\)\.git/\1/')/pull/new/$CURRENT_BRANCH"
fi
echo "Task completion workflow finished!"
```
--------------------------------------------------------------------------------
/src/features/repositories/get-repository/feature.spec.int.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { getRepository } from './feature';
import {
getTestConnection,
shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';
describe('getRepository integration', () => {
let connection: WebApi | null = null;
let projectName: string;
beforeAll(async () => {
// Get a real connection using environment variables
connection = await getTestConnection();
projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
});
test('should retrieve a real repository from Azure DevOps', async () => {
// Skip if no connection is available
if (shouldSkipIntegrationTest()) {
return;
}
// This connection must be available if we didn't skip
if (!connection) {
throw new Error(
'Connection should be available when test is not skipped',
);
}
// First, get a list of repos to find one to test with
const gitApi = await connection.getGitApi();
const repos = await gitApi.getRepositories(projectName);
// Skip if no repos are available
if (!repos || repos.length === 0) {
console.log('Skipping test: No repositories available in the project');
return;
}
// Use the first repo as a test subject
const testRepo = repos[0];
// Act - make an actual API call to Azure DevOps
const result = await getRepository(
connection,
projectName,
testRepo.name || testRepo.id || '',
);
// Assert on the actual response
expect(result).toBeDefined();
expect(result.id).toBe(testRepo.id);
expect(result.name).toBe(testRepo.name);
expect(result.project).toBeDefined();
if (result.project) {
expect(result.project.name).toBe(projectName);
}
});
test('should throw error when repository is not found', async () => {
// Skip if no connection is available
if (shouldSkipIntegrationTest()) {
return;
}
// This connection must be available if we didn't skip
if (!connection) {
throw new Error(
'Connection should be available when test is not skipped',
);
}
// Use a non-existent repository name
const nonExistentRepoName = 'non-existent-repo-' + Date.now();
// Act & Assert - should throw an error for non-existent repo
await expect(
getRepository(connection, projectName, nonExistentRepoName),
).rejects.toThrow(/not found|Failed to get repository/);
});
});
```
--------------------------------------------------------------------------------
/src/features/repositories/list-repositories/feature.spec.int.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { listRepositories } from './feature';
import {
getTestConnection,
shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';
import { ListRepositoriesOptions } from '../types';
describe('listRepositories integration', () => {
let connection: WebApi | null = null;
let projectName: string;
beforeAll(async () => {
// Get a real connection using environment variables
connection = await getTestConnection();
projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject';
});
test('should list repositories in a project', 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',
);
}
const options: ListRepositoriesOptions = {
projectId: projectName,
};
// Act - make an actual API call to Azure DevOps
const result = await listRepositories(connection, options);
// Assert on the actual response
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
// Check structure of returned items (even if empty)
if (result.length > 0) {
const firstRepo = result[0];
expect(firstRepo.id).toBeDefined();
expect(firstRepo.name).toBeDefined();
expect(firstRepo.project).toBeDefined();
if (firstRepo.project) {
expect(firstRepo.project.name).toBe(projectName);
}
}
});
test('should include links when option is specified', 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',
);
}
const options: ListRepositoriesOptions = {
projectId: projectName,
includeLinks: true,
};
// Act - make an actual API call to Azure DevOps
const result = await listRepositories(connection, options);
// Assert on the actual response
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
// Verify links are included, if repositories exist
if (result.length > 0) {
const firstRepo = result[0];
expect(firstRepo._links).toBeDefined();
}
});
});
```
--------------------------------------------------------------------------------
/src/features/projects/list-projects/feature.spec.int.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { listProjects } from './feature';
import {
getTestConnection,
shouldSkipIntegrationTest,
} from '@/shared/test/test-helpers';
import { ListProjectsOptions } from '../types';
describe('listProjects integration', () => {
let connection: WebApi | null = null;
beforeAll(async () => {
// Get a real connection using environment variables
connection = await getTestConnection();
});
test('should list projects in the organization', async () => {
// Skip if no connection is available
if (shouldSkipIntegrationTest()) {
return;
}
// This connection must be available if we didn't skip
if (!connection) {
throw new Error(
'Connection should be available when test is not skipped',
);
}
// Act - make an actual API call to Azure DevOps
const result = await listProjects(connection);
// Assert on the actual response
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
// Check structure of returned items (even if empty)
if (result.length > 0) {
const firstProject = result[0];
expect(firstProject.id).toBeDefined();
expect(firstProject.name).toBeDefined();
expect(firstProject.url).toBeDefined();
expect(firstProject.state).toBeDefined();
}
});
test('should apply pagination options', 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',
);
}
const options: ListProjectsOptions = {
top: 2, // Only get up to 2 projects
};
// Act - make an actual API call to Azure DevOps
const result = await listProjects(connection, options);
// Assert on the actual response
expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeLessThanOrEqual(2);
// If we have projects, check for correct limit
if (result.length > 0) {
// Get all projects to compare
const allProjects = await listProjects(connection);
// If we have more than 2 total projects, pagination should have limited results
if (allProjects.length > 2) {
expect(result.length).toBe(2);
expect(result.length).toBeLessThan(allProjects.length);
}
}
});
});
```
--------------------------------------------------------------------------------
/src/features/users/index.spec.unit.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
import { isUsersRequest, handleUsersRequest } from './index';
import { getMe } from './get-me';
// Mock the imported modules
jest.mock('./get-me', () => ({
getMe: jest.fn(),
}));
describe('Users Request Handlers', () => {
const mockConnection = {} as WebApi;
describe('isUsersRequest', () => {
it('should return true for users requests', () => {
const request = {
params: { name: 'get_me', arguments: {} },
method: 'tools/call',
} as CallToolRequest;
expect(isUsersRequest(request)).toBe(true);
});
it('should return false for non-users requests', () => {
const request = {
params: { name: 'list_projects', arguments: {} },
method: 'tools/call',
} as CallToolRequest;
expect(isUsersRequest(request)).toBe(false);
});
});
describe('handleUsersRequest', () => {
it('should handle get_me request', async () => {
const mockUserProfile = {
id: 'user-id-123',
displayName: 'Test User',
email: '[email protected]',
};
(getMe as jest.Mock).mockResolvedValue(mockUserProfile);
const request = {
params: {
name: 'get_me',
arguments: {},
},
method: 'tools/call',
} as CallToolRequest;
const response = await handleUsersRequest(mockConnection, request);
expect(response.content).toHaveLength(1);
expect(JSON.parse(response.content[0].text as string)).toEqual(
mockUserProfile,
);
expect(getMe).toHaveBeenCalledWith(mockConnection);
});
it('should throw error for unknown tool', async () => {
const request = {
params: {
name: 'unknown_tool',
arguments: {},
},
method: 'tools/call',
} as CallToolRequest;
await expect(handleUsersRequest(mockConnection, request)).rejects.toThrow(
'Unknown users tool',
);
});
it('should propagate errors from user functions', async () => {
const mockError = new Error('Test error');
(getMe as jest.Mock).mockRejectedValue(mockError);
const request = {
params: {
name: 'get_me',
arguments: {},
},
method: 'tools/call',
} as CallToolRequest;
await expect(handleUsersRequest(mockConnection, request)).rejects.toThrow(
mockError,
);
});
});
});
```
--------------------------------------------------------------------------------
/src/features/repositories/get-repository-tree/feature.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import {
GitVersionType,
VersionControlRecursionType,
GitObjectType,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { AzureDevOpsError } from '../../../shared/errors';
import {
GetRepositoryTreeOptions,
RepositoryTreeItem,
RepositoryTreeResponse,
} from '../types';
/**
* Get tree view of files/directories in a repository starting at an optional path
*/
export async function getRepositoryTree(
connection: WebApi,
options: GetRepositoryTreeOptions,
): Promise<RepositoryTreeResponse> {
try {
const gitApi = await connection.getGitApi();
const repository = await gitApi.getRepository(
options.repositoryId,
options.projectId,
);
if (!repository || !repository.id) {
throw new AzureDevOpsError(
`Repository '${options.repositoryId}' not found in project '${options.projectId}'`,
);
}
const defaultBranch = repository.defaultBranch;
if (!defaultBranch) {
throw new AzureDevOpsError('Default branch not found');
}
const branchRef = defaultBranch.replace('refs/heads/', '');
const rootPath = options.path ?? '/';
const items = await gitApi.getItems(
repository.id,
options.projectId,
rootPath,
VersionControlRecursionType.Full,
true,
false,
false,
false,
{
version: branchRef,
versionType: GitVersionType.Branch,
},
);
const treeItems: RepositoryTreeItem[] = [];
const stats = { directories: 0, files: 0 };
for (const item of items) {
const path = item.path || '';
if (path === rootPath || item.gitObjectType === GitObjectType.Bad) {
continue;
}
const relative =
rootPath === '/'
? path.replace(/^\//, '')
: path.slice(rootPath.length + 1);
const level = relative.split('/').length;
if (options.depth && options.depth > 0 && level > options.depth) {
continue;
}
const isFolder = !!item.isFolder;
treeItems.push({
name: relative.split('/').pop() || '',
path,
isFolder,
level,
});
if (isFolder) stats.directories++;
else stats.files++;
}
return {
name: repository.name || options.repositoryId,
tree: treeItems,
stats,
};
} catch (error) {
if (error instanceof AzureDevOpsError) {
throw error;
}
throw new Error(
`Failed to get repository tree: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
```
--------------------------------------------------------------------------------
/src/features/repositories/get-repository-details/feature.ts:
--------------------------------------------------------------------------------
```typescript
import { WebApi } from 'azure-devops-node-api';
import { GitVersionType } from 'azure-devops-node-api/interfaces/GitInterfaces';
import {
AzureDevOpsResourceNotFoundError,
AzureDevOpsError,
} from '../../../shared/errors';
import { GetRepositoryDetailsOptions, RepositoryDetails } from '../types';
/**
* Get detailed information about a repository
*
* @param connection The Azure DevOps WebApi connection
* @param options Options for getting repository details
* @returns The repository details including optional statistics and refs
* @throws {AzureDevOpsResourceNotFoundError} If the repository is not found
*/
export async function getRepositoryDetails(
connection: WebApi,
options: GetRepositoryDetailsOptions,
): Promise<RepositoryDetails> {
try {
const gitApi = await connection.getGitApi();
// Get the basic repository information
const repository = await gitApi.getRepository(
options.repositoryId,
options.projectId,
);
if (!repository) {
throw new AzureDevOpsResourceNotFoundError(
`Repository '${options.repositoryId}' not found in project '${options.projectId}'`,
);
}
// Initialize the response object
const response: RepositoryDetails = {
repository,
};
// Get branch statistics if requested
if (options.includeStatistics) {
let baseVersionDescriptor = undefined;
// If a specific branch name is provided, create a version descriptor for it
if (options.branchName) {
baseVersionDescriptor = {
version: options.branchName,
versionType: GitVersionType.Branch,
};
}
const branchStats = await gitApi.getBranches(
repository.id || '',
options.projectId,
baseVersionDescriptor,
);
response.statistics = {
branches: branchStats || [],
};
}
// Get repository refs if requested
if (options.includeRefs) {
const filter = options.refFilter || undefined;
const refs = await gitApi.getRefs(
repository.id || '',
options.projectId,
filter,
);
if (refs) {
response.refs = {
value: refs,
count: refs.length,
};
} else {
response.refs = {
value: [],
count: 0,
};
}
}
return response;
} catch (error) {
if (error instanceof AzureDevOpsError) {
throw error;
}
throw new Error(
`Failed to get repository details: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
```
--------------------------------------------------------------------------------
/src/shared/enums/index.ts:
--------------------------------------------------------------------------------
```typescript
import {
CommentThreadStatus,
CommentType,
GitVersionType,
} from 'azure-devops-node-api/interfaces/GitInterfaces';
import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces';
/**
* Generic enum mapper that creates bidirectional mappings between strings and numeric enums
*/
function createEnumMapper(
mappings: Record<string, number>,
defaultStringValue = 'unknown',
) {
// Create reverse mapping from enum values to strings
const reverseMap = Object.entries(mappings).reduce(
(acc, [key, value]) => {
acc[value] = key;
return acc;
},
{} as Record<number, string>,
);
return {
toEnum: (value: string): number | undefined => {
const lowerValue = value.toLowerCase();
return mappings[lowerValue];
},
toString: (value: number): string => {
return reverseMap[value] ?? defaultStringValue;
},
};
}
/**
* CommentThreadStatus enum mappings
*/
export const commentThreadStatusMapper = createEnumMapper({
unknown: CommentThreadStatus.Unknown,
active: CommentThreadStatus.Active,
fixed: CommentThreadStatus.Fixed,
wontfix: CommentThreadStatus.WontFix,
closed: CommentThreadStatus.Closed,
bydesign: CommentThreadStatus.ByDesign,
pending: CommentThreadStatus.Pending,
});
/**
* CommentType enum mappings
*/
export const commentTypeMapper = createEnumMapper({
unknown: CommentType.Unknown,
text: CommentType.Text,
codechange: CommentType.CodeChange,
system: CommentType.System,
});
/**
* PullRequestStatus enum mappings
*/
export const pullRequestStatusMapper = createEnumMapper({
active: PullRequestStatus.Active,
abandoned: PullRequestStatus.Abandoned,
completed: PullRequestStatus.Completed,
});
/**
* GitVersionType enum mappings
*/
export const gitVersionTypeMapper = createEnumMapper({
branch: GitVersionType.Branch,
commit: GitVersionType.Commit,
tag: GitVersionType.Tag,
});
/**
* Transform comment thread status from numeric to string
*/
export function transformCommentThreadStatus(
status?: number,
): string | undefined {
return status !== undefined
? commentThreadStatusMapper.toString(status)
: undefined;
}
/**
* Transform comment type from numeric to string
*/
export function transformCommentType(type?: number): string | undefined {
return type !== undefined ? commentTypeMapper.toString(type) : undefined;
}
/**
* Transform pull request status from numeric to string
*/
export function transformPullRequestStatus(
status?: number,
): string | undefined {
return status !== undefined
? pullRequestStatusMapper.toString(status)
: undefined;
}
```
--------------------------------------------------------------------------------
/src/features/projects/index.ts:
--------------------------------------------------------------------------------
```typescript
// Re-export schemas and types
export * from './schemas';
export * from './types';
// Re-export features
export * from './get-project';
export * from './get-project-details';
export * from './list-projects';
// 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 {
GetProjectSchema,
GetProjectDetailsSchema,
ListProjectsSchema,
getProject,
getProjectDetails,
listProjects,
} from './';
/**
* Checks if the request is for the projects feature
*/
export const isProjectsRequest: RequestIdentifier = (
request: CallToolRequest,
): boolean => {
const toolName = request.params.name;
return ['list_projects', 'get_project', 'get_project_details'].includes(
toolName,
);
};
/**
* Handles projects feature requests
*/
export const handleProjectsRequest: RequestHandler = async (
connection: WebApi,
request: CallToolRequest,
): Promise<{ content: Array<{ type: string; text: string }> }> => {
switch (request.params.name) {
case 'list_projects': {
const args = ListProjectsSchema.parse(request.params.arguments);
const result = await listProjects(connection, {
stateFilter: args.stateFilter,
top: args.top,
skip: args.skip,
continuationToken: args.continuationToken,
});
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
}
case 'get_project': {
const args = GetProjectSchema.parse(request.params.arguments);
const result = await getProject(
connection,
args.projectId ?? defaultProject,
);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
}
case 'get_project_details': {
const args = GetProjectDetailsSchema.parse(request.params.arguments);
const result = await getProjectDetails(connection, {
projectId: args.projectId ?? defaultProject,
includeProcess: args.includeProcess,
includeWorkItemTypes: args.includeWorkItemTypes,
includeFields: args.includeFields,
includeTeams: args.includeTeams,
expandTeamIdentity: args.expandTeamIdentity,
});
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
}
default:
throw new Error(`Unknown projects tool: ${request.params.name}`);
}
};
```
--------------------------------------------------------------------------------
/src/index.spec.unit.ts:
--------------------------------------------------------------------------------
```typescript
import { normalizeAuthMethod } from './index';
import { AuthenticationMethod } from './shared/auth/auth-factory';
describe('index', () => {
describe('normalizeAuthMethod', () => {
it('should return AzureIdentity when authMethodStr is undefined', () => {
// Arrange
const authMethodStr = undefined;
// Act
const result = normalizeAuthMethod(authMethodStr);
// Assert
expect(result).toBe(AuthenticationMethod.AzureIdentity);
});
it('should return AzureIdentity when authMethodStr is empty', () => {
// Arrange
const authMethodStr = '';
// Act
const result = normalizeAuthMethod(authMethodStr);
// Assert
expect(result).toBe(AuthenticationMethod.AzureIdentity);
});
it('should handle PersonalAccessToken case-insensitively', () => {
// Arrange
const variations = ['pat', 'PAT', 'Pat', 'pAt', 'paT'];
// Act & Assert
variations.forEach((variant) => {
expect(normalizeAuthMethod(variant)).toBe(
AuthenticationMethod.PersonalAccessToken,
);
});
});
it('should handle AzureIdentity case-insensitively', () => {
// Arrange
const variations = [
'azure-identity',
'AZURE-IDENTITY',
'Azure-Identity',
'azure-Identity',
'Azure-identity',
];
// Act & Assert
variations.forEach((variant) => {
expect(normalizeAuthMethod(variant)).toBe(
AuthenticationMethod.AzureIdentity,
);
});
});
it('should handle AzureCli case-insensitively', () => {
// Arrange
const variations = [
'azure-cli',
'AZURE-CLI',
'Azure-Cli',
'azure-Cli',
'Azure-cli',
];
// Act & Assert
variations.forEach((variant) => {
expect(normalizeAuthMethod(variant)).toBe(
AuthenticationMethod.AzureCli,
);
});
});
it('should return AzureIdentity for unrecognized values', () => {
// Arrange
const unrecognized = [
'unknown',
'azureCli', // no hyphen
'azureIdentity', // no hyphen
'personal-access-token', // not matching enum value
'cli',
'identity',
];
// Act & Assert (mute stderr for warning messages)
const originalStderrWrite = process.stderr.write;
process.stderr.write = jest.fn();
try {
unrecognized.forEach((value) => {
expect(normalizeAuthMethod(value)).toBe(
AuthenticationMethod.AzureIdentity,
);
});
} finally {
process.stderr.write = originalStderrWrite;
}
});
});
});
```