This is page 1 of 8. Use http://codebase.md/tiberriver256/azure-devops-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .clinerules ├── .env.example ├── .eslintrc.json ├── .github │ ├── FUNDING.yml │ ├── release-please-config.json │ ├── release-please-manifest.json │ └── workflows │ ├── main.yml │ └── release-please.yml ├── .gitignore ├── .husky │ ├── commit-msg │ └── pre-commit ├── .kilocode │ └── mcp.json ├── .prettierrc ├── .vscode │ └── settings.json ├── CHANGELOG.md ├── commitlint.config.js ├── CONTRIBUTING.md ├── create_branch.sh ├── docs │ ├── authentication.md │ ├── azure-identity-authentication.md │ ├── ci-setup.md │ ├── examples │ │ ├── azure-cli-authentication.env │ │ ├── azure-identity-authentication.env │ │ ├── pat-authentication.env │ │ └── README.md │ ├── testing │ │ ├── README.md │ │ └── setup.md │ └── tools │ ├── core-navigation.md │ ├── organizations.md │ ├── pipelines.md │ ├── projects.md │ ├── pull-requests.md │ ├── README.md │ ├── repositories.md │ ├── resources.md │ ├── search.md │ ├── user-tools.md │ ├── wiki.md │ └── work-items.md ├── finish_task.sh ├── jest.e2e.config.js ├── jest.int.config.js ├── jest.unit.config.js ├── LICENSE ├── memory │ └── tasks_memory_2025-05-26T16-18-03.json ├── package-lock.json ├── package.json ├── project-management │ ├── planning │ │ ├── architecture-guide.md │ │ ├── azure-identity-authentication-design.md │ │ ├── project-plan.md │ │ ├── project-structure.md │ │ ├── tech-stack.md │ │ └── the-dream-team.md │ ├── startup.xml │ ├── tdd-cycle.xml │ └── troubleshooter.xml ├── README.md ├── setup_env.sh ├── shrimp-rules.md ├── src │ ├── clients │ │ └── azure-devops.ts │ ├── features │ │ ├── organizations │ │ │ ├── __test__ │ │ │ │ └── test-helpers.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── list-organizations │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── schemas.ts │ │ │ ├── tool-definitions.ts │ │ │ └── types.ts │ │ ├── pipelines │ │ │ ├── get-pipeline │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── list-pipelines │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── tool-definitions.ts │ │ │ ├── trigger-pipeline │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ └── types.ts │ │ ├── projects │ │ │ ├── __test__ │ │ │ │ └── test-helpers.ts │ │ │ ├── get-project │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-project-details │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── list-projects │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── schemas.ts │ │ │ ├── tool-definitions.ts │ │ │ └── types.ts │ │ ├── pull-requests │ │ │ ├── add-pull-request-comment │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ └── index.ts │ │ │ ├── create-pull-request │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-pull-request-comments │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ └── index.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── list-pull-requests │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── schemas.ts │ │ │ ├── tool-definitions.ts │ │ │ ├── types.ts │ │ │ └── update-pull-request │ │ │ ├── feature.spec.int.ts │ │ │ ├── feature.spec.unit.ts │ │ │ ├── feature.ts │ │ │ └── index.ts │ │ ├── repositories │ │ │ ├── __test__ │ │ │ │ └── test-helpers.ts │ │ │ ├── get-all-repositories-tree │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── feature.spec.unit.ts.snap │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-file-content │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-repository │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-repository-details │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── list-repositories │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── schemas.ts │ │ │ ├── tool-definitions.ts │ │ │ └── types.ts │ │ ├── search │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── schemas.ts │ │ │ ├── search-code │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ └── index.ts │ │ │ ├── search-wiki │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ └── index.ts │ │ │ ├── search-work-items │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ └── index.ts │ │ │ ├── tool-definitions.ts │ │ │ └── types.ts │ │ ├── users │ │ │ ├── get-me │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── schemas.ts │ │ │ ├── tool-definitions.ts │ │ │ └── types.ts │ │ ├── wikis │ │ │ ├── create-wiki │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── create-wiki-page │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-wiki-page │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── get-wikis │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── index.spec.unit.ts │ │ │ ├── index.ts │ │ │ ├── list-wiki-pages │ │ │ │ ├── feature.spec.int.ts │ │ │ │ ├── feature.spec.unit.ts │ │ │ │ ├── feature.ts │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ ├── tool-definitions.ts │ │ │ └── update-wiki-page │ │ │ ├── feature.spec.int.ts │ │ │ ├── feature.ts │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ └── work-items │ │ ├── __test__ │ │ │ ├── fixtures.ts │ │ │ ├── test-helpers.ts │ │ │ └── test-utils.ts │ │ ├── create-work-item │ │ │ ├── feature.spec.int.ts │ │ │ ├── feature.spec.unit.ts │ │ │ ├── feature.ts │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ ├── get-work-item │ │ │ ├── feature.spec.int.ts │ │ │ ├── feature.spec.unit.ts │ │ │ ├── feature.ts │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ ├── index.spec.unit.ts │ │ ├── index.ts │ │ ├── list-work-items │ │ │ ├── feature.spec.int.ts │ │ │ ├── feature.spec.unit.ts │ │ │ ├── feature.ts │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ ├── manage-work-item-link │ │ │ ├── feature.spec.int.ts │ │ │ ├── feature.spec.unit.ts │ │ │ ├── feature.ts │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ ├── schemas.ts │ │ ├── tool-definitions.ts │ │ ├── types.ts │ │ └── update-work-item │ │ ├── feature.spec.int.ts │ │ ├── feature.spec.unit.ts │ │ ├── feature.ts │ │ ├── index.ts │ │ └── schema.ts │ ├── index.spec.unit.ts │ ├── index.ts │ ├── server.spec.e2e.ts │ ├── server.ts │ ├── shared │ │ ├── api │ │ │ ├── client.ts │ │ │ └── index.ts │ │ ├── auth │ │ │ ├── auth-factory.ts │ │ │ ├── client-factory.ts │ │ │ └── index.ts │ │ ├── config │ │ │ ├── index.ts │ │ │ └── version.ts │ │ ├── enums │ │ │ ├── index.spec.unit.ts │ │ │ └── index.ts │ │ ├── errors │ │ │ ├── azure-devops-errors.ts │ │ │ ├── handle-request-error.ts │ │ │ └── index.ts │ │ ├── test │ │ │ └── test-helpers.ts │ │ └── types │ │ ├── config.ts │ │ ├── index.ts │ │ ├── request-handler.ts │ │ └── tool-definition.ts │ └── utils │ ├── environment.spec.unit.ts │ └── environment.ts ├── tasks.json ├── tests │ └── setup.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- ``` 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "tabWidth": 2, 7 | "useTabs": false, 8 | "endOfLine": "lf" 9 | } 10 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # TypeScript 8 | dist/ 9 | *.tsbuildinfo 10 | 11 | # Environment variables 12 | .env 13 | .env.local 14 | .env.*.local 15 | 16 | # IDE 17 | .vscode/* 18 | !.vscode/extensions.json 19 | !.vscode/settings.json 20 | .idea/ 21 | *.swp 22 | *.swo 23 | 24 | # OS 25 | .DS_Store 26 | Thumbs.db 27 | 28 | # Test coverage 29 | coverage/ 30 | 31 | # Logs 32 | logs/ 33 | *.log ``` -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 5 | "env": { 6 | "node": true, 7 | "es6": true, 8 | "jest": true 9 | }, 10 | "rules": { 11 | "@typescript-eslint/explicit-function-return-type": "off", 12 | "@typescript-eslint/no-explicit-any": "warn", 13 | "@typescript-eslint/no-unused-vars": [ 14 | "error", 15 | { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } 16 | ] 17 | }, 18 | "overrides": [ 19 | { 20 | "files": ["**/*.spec.unit.ts", "tests/**/*.ts"], 21 | "rules": { 22 | "@typescript-eslint/no-explicit-any": "off" 23 | } 24 | } 25 | ], 26 | "parserOptions": { 27 | "ecmaVersion": 2020, 28 | "sourceType": "module" 29 | }, 30 | "ignorePatterns": ["dist/**/*", "project-management/**/*"] 31 | } 32 | ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Azure DevOps MCP Server - Environment Variables 2 | 3 | # Azure DevOps Organization URL (required) 4 | # e.g., https://dev.azure.com/your-organization 5 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization 6 | 7 | # Authentication Method (optional, defaults to 'azure-identity') 8 | # Supported values: 'pat', 'azure-identity', 'azure-cli' 9 | # - 'pat': Personal Access Token authentication 10 | # - 'azure-identity': Azure Identity authentication (DefaultAzureCredential) 11 | # - 'azure-cli': Azure CLI authentication (AzureCliCredential) 12 | AZURE_DEVOPS_AUTH_METHOD=azure-identity 13 | 14 | # Azure DevOps Personal Access Token (required for PAT authentication) 15 | # Create one at: https://dev.azure.com/your-organization/_usersSettings/tokens 16 | # Required scopes: Code (Read & Write), Work Items (Read & Write), Build (Read & Execute), 17 | # Project and Team (Read), Graph (Read), Release (Read & Execute) 18 | AZURE_DEVOPS_PAT=your-personal-access-token 19 | 20 | # Default Project to use when not specified (optional) 21 | AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project 22 | 23 | # API Version to use (optional, defaults to latest) 24 | # AZURE_DEVOPS_API_VERSION=6.0 25 | 26 | # Note: This server uses stdio for communication, not HTTP 27 | # The following variables are not used by the server but might be used by scripts: 28 | 29 | # Logging Level (debug, info, warn, error) 30 | LOG_LEVEL=info 31 | 32 | # Azure Identity Credentials (for service principal authentication) 33 | # Required only when using azure-identity with service principals 34 | # AZURE_TENANT_ID=your-tenant-id 35 | # AZURE_CLIENT_ID=your-client-id 36 | # AZURE_CLIENT_SECRET=your-client-secret ``` -------------------------------------------------------------------------------- /.clinerules: -------------------------------------------------------------------------------- ``` 1 | # MCP Server Development Protocol 2 | 3 | ⚠️ CRITICAL: DO NOT USE attempt_completion BEFORE TESTING ⚠️ 4 | 5 | ## Commit Rules 6 | 7 | ⚠️ 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'. 8 | 9 | ⚠️ RECOMMENDED: Use 'npm run commit' to create commit messages interactively, ensuring compliance. 10 | 11 | ## Step 1: Planning (PLAN MODE) 12 | - What problem does this tool solve? 13 | - What API/service will it use? 14 | - What are the authentication requirements? 15 | □ Standard API key 16 | □ OAuth (requires separate setup script) 17 | □ Other credentials 18 | 19 | ## Step 2: Implementation (ACT MODE) 20 | 1. Bootstrap 21 | - For web services, JavaScript integration, or Node.js environments: 22 | ```bash 23 | npx @modelcontextprotocol/create-server my-server 24 | cd my-server 25 | npm install 26 | ``` 27 | - For data science, ML workflows, or Python environments: 28 | ```bash 29 | pip install mcp 30 | # Or with uv (recommended) 31 | uv add "mcp[cli]" 32 | ``` 33 | 34 | 2. Core Implementation 35 | - Use MCP SDK 36 | - Implement comprehensive logging 37 | - TypeScript (for web/JS projects): 38 | ```typescript 39 | console.error('[Setup] Initializing server...'); 40 | console.error('[API] Request to endpoint:', endpoint); 41 | console.error('[Error] Failed with:', error); 42 | ``` 43 | - Python (for data science/ML projects): 44 | ```python 45 | import logging 46 | logging.error('[Setup] Initializing server...') 47 | logging.error(f'[API] Request to endpoint: {endpoint}') 48 | logging.error(f'[Error] Failed with: {str(error)}') 49 | ``` 50 | - Add type definitions 51 | - Handle errors with context 52 | - Implement rate limiting if needed 53 | 54 | 3. Configuration 55 | - Get credentials from user if needed 56 | - Add to MCP settings: 57 | - For TypeScript projects: 58 | ```json 59 | { 60 | "mcpServers": { 61 | "my-server": { 62 | "command": "node", 63 | "args": ["path/to/build/index.js"], 64 | "env": { 65 | "API_KEY": "key" 66 | }, 67 | "disabled": false, 68 | "autoApprove": [] 69 | } 70 | } 71 | } 72 | ``` 73 | - For Python projects: 74 | ```bash 75 | # Directly with command line 76 | mcp install server.py -v API_KEY=key 77 | 78 | # Or in settings.json 79 | { 80 | "mcpServers": { 81 | "my-server": { 82 | "command": "python", 83 | "args": ["server.py"], 84 | "env": { 85 | "API_KEY": "key" 86 | }, 87 | "disabled": false, 88 | "autoApprove": [] 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | ## Step 3: Testing (BLOCKER ⛔️) 95 | 96 | <thinking> 97 | BEFORE using attempt_completion, I MUST verify: 98 | □ Have I tested EVERY tool? 99 | □ Have I confirmed success from the user for each test? 100 | □ Have I documented the test results? 101 | 102 | If ANY answer is "no", I MUST NOT use attempt_completion. 103 | </thinking> 104 | 105 | 1. Test Each Tool (REQUIRED) 106 | □ Test each tool with valid inputs 107 | □ Verify output format is correct 108 | ⚠️ DO NOT PROCEED UNTIL ALL TOOLS TESTED 109 | 110 | ## Step 4: Completion 111 | ❗ STOP AND VERIFY: 112 | □ Every tool has been tested with valid inputs 113 | □ Output format is correct for each tool 114 | 115 | Only after ALL tools have been tested can attempt_completion be used. 116 | 117 | ## Key Requirements 118 | - ✓ Must use MCP SDK 119 | - ✓ Must have comprehensive logging 120 | - ✓ Must test each tool individually 121 | - ✓ Must handle errors gracefully 122 | - ⛔️ NEVER skip testing before completion ``` -------------------------------------------------------------------------------- /docs/examples/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Authentication Examples 2 | 3 | This directory contains example `.env` files for different authentication methods supported by the Azure DevOps MCP Server. 4 | 5 | ## Available Examples 6 | 7 | 1. **[pat-authentication.env](./pat-authentication.env)** - Example configuration for Personal Access Token (PAT) authentication 8 | 2. **[azure-identity-authentication.env](./azure-identity-authentication.env)** - Example configuration for Azure Identity (DefaultAzureCredential) authentication 9 | 3. **[azure-cli-authentication.env](./azure-cli-authentication.env)** - Example configuration for Azure CLI authentication 10 | 11 | ## How to Use These Examples 12 | 13 | 1. Choose the authentication method that best suits your needs 14 | 2. Copy the corresponding example file to the root of your project as `.env` 15 | 3. Replace the placeholder values with your actual values 16 | 4. Start the Azure DevOps MCP Server 17 | 18 | For example: 19 | 20 | ```bash 21 | # Copy the PAT authentication example 22 | cp docs/examples/pat-authentication.env .env 23 | 24 | # Edit the .env file with your values 25 | nano .env 26 | 27 | # Start the server 28 | npm start 29 | ``` 30 | 31 | ## Additional Resources 32 | 33 | For more detailed information about authentication methods, setup instructions, and troubleshooting, refer to the [Authentication Guide](../authentication.md). 34 | ``` -------------------------------------------------------------------------------- /docs/tools/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Azure DevOps MCP Server Tools Documentation 2 | 3 | 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. 4 | 5 | ## Navigation 6 | 7 | - [Core Navigation Tools](./core-navigation.md) - Overview of tools for navigating Azure DevOps resources 8 | - [Organizations](./organizations.md) - Tools for working with organizations 9 | - [Projects](./projects.md) - Tools for working with projects 10 | - [Repositories](./repositories.md) - Tools for working with Git repositories 11 | - [Pull Requests](./pull-requests.md) - Tools for working with pull requests 12 | - [Work Items](./work-items.md) - Tools for working with work items 13 | - [Pipelines](./pipelines.md) - Tools for working with pipelines 14 | - [Resource URIs](./resources.md) - Documentation for accessing repository content via resource URIs 15 | 16 | ## Tools by Category 17 | 18 | ### Organization Tools 19 | 20 | - [`list_organizations`](./organizations.md#list_organizations) - List all Azure DevOps organizations accessible to the user 21 | 22 | ### Project Tools 23 | 24 | - [`list_projects`](./projects.md#list_projects) - List all projects in the organization 25 | - [`get_project`](./projects.md#get_project) - Get details of a specific project 26 | 27 | ### Repository Tools 28 | 29 | - [`list_repositories`](./repositories.md#list_repositories) - List all repositories in a project 30 | - [`get_repository`](./repositories.md#get_repository) - Get details of a specific repository 31 | - [`get_repository_details`](./repositories.md#get_repository_details) - Get detailed information about a repository 32 | - [`get_file_content`](./repositories.md#get_file_content) - Get content of a file or directory from a repository 33 | 34 | ### Pull Request Tools 35 | 36 | - [`create_pull_request`](./pull-requests.md#create_pull_request) - Create a new pull request 37 | - [`list_pull_requests`](./pull-requests.md#list_pull_requests) - List pull requests in a repository 38 | - [`add_pull_request_comment`](./pull-requests.md#add_pull_request_comment) - Add a comment to a pull request 39 | - [`get_pull_request_comments`](./pull-requests.md#get_pull_request_comments) - Get comments from a pull request 40 | - [`update_pull_request`](./pull-requests.md#update_pull_request) - Update an existing pull request (title, description, status, draft state, reviewers, work items) 41 | 42 | ### Work Item Tools 43 | 44 | - [`get_work_item`](./work-items.md#get_work_item) - Retrieve a work item by ID 45 | - [`create_work_item`](./work-items.md#create_work_item) - Create a new work item 46 | - [`list_work_items`](./work-items.md#list_work_items) - List work items in a project 47 | 48 | ### Pipeline Tools 49 | 50 | - [`list_pipelines`](./pipelines.md#list_pipelines) - List all pipelines in a project 51 | - [`get_pipeline`](./pipelines.md#get_pipeline) - Get details of a specific pipeline 52 | 53 | ## Tool Structure 54 | 55 | Each tool documentation follows a consistent structure: 56 | 57 | 1. **Description**: Brief explanation of what the tool does 58 | 2. **Parameters**: Required and optional parameters with explanations 59 | 3. **Response**: Expected response format with examples 60 | 4. **Error Handling**: Potential errors and how they're handled 61 | 5. **Example Usage**: Code examples showing how to use the tool 62 | 6. **Implementation Details**: Technical details about how the tool works 63 | 64 | ## Examples 65 | 66 | Examples of using multiple tools together can be found in the [Core Navigation Tools](./core-navigation.md#common-use-cases) documentation. 67 | ``` -------------------------------------------------------------------------------- /docs/testing/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Testing Trophy Approach 2 | 3 | ## Overview 4 | 5 | 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. 6 | 7 |  8 | 9 | ## Key Principles 10 | 11 | 1. **"The more your tests resemble the way your software is used, the more confidence they can give you."** - Kent C. Dodds 12 | 2. Focus on testing behavior and interfaces rather than implementation details 13 | 3. Maximize return on investment where "return" is confidence and "investment" is time 14 | 4. Use arrange/act/assert pattern for all tests 15 | 5. Co-locate tests with the code they test following Feature Sliced Design 16 | 17 | ## Test Types 18 | 19 | ### Static Analysis (The Base) 20 | 21 | - TypeScript for type checking 22 | - ESLint for code quality and consistency 23 | - Runtime type checking with Zod 24 | - Formatter (Prettier) 25 | 26 | These tools catch many issues before tests are even run and provide immediate feedback during development. 27 | 28 | ### Unit Tests (Small Layer) 29 | 30 | - Located in `*.spec.unit.ts` files 31 | - Co-located with the code they test 32 | - Focus on testing complex business logic in isolation 33 | - Minimal mocking where necessary 34 | - Run with `npm run test:unit` 35 | 36 | Unit tests should be used sparingly for complex logic that requires isolated testing. We don't aim for 100% coverage with unit tests. 37 | 38 | ### Integration Tests (Main Focus) 39 | 40 | - Located in `*.spec.int.ts` files 41 | - Co-located with the features they test 42 | - Test how modules work together 43 | - Focus on testing behavior, not implementation 44 | - Run with `npm run test:int` 45 | 46 | These provide the bulk of our test coverage and confidence. They verify that different parts of the system work together correctly. 47 | 48 | ### End-to-End Tests (Small Cap) 49 | 50 | - Located in `*.spec.e2e.ts` files 51 | - **Only exists at the server level** (e.g., `server.spec.e2e.ts`) where they use the MCP client 52 | - Test complete user flows across the entire application 53 | - Provide the highest confidence but are slower and more costly to maintain 54 | - Run with `npm run test:e2e` 55 | 56 | 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. 57 | 58 | For testing interactions with external APIs like Azure DevOps, use integration tests (`*.spec.int.ts`) instead, which are co-located with the feature implementations. 59 | 60 | ## Test File Naming Convention 61 | 62 | - `*.spec.unit.ts` - For minimal unit tests (essential logic only) 63 | - `*.spec.int.ts` - For integration tests (main focus) 64 | - `*.spec.e2e.ts` - For end-to-end tests 65 | 66 | ## Test Location 67 | 68 | We co-locate unit and integration tests with the code they're testing following Feature Sliced Design principles: 69 | 70 | ``` 71 | src/ 72 | features/ 73 | feature-name/ 74 | feature.ts 75 | feature.spec.unit.ts # Unit tests 76 | feature.spec.int.ts # Integration tests 77 | ``` 78 | 79 | E2E tests are only located at the server level since they test the full application: 80 | 81 | ``` 82 | src/ 83 | server.ts 84 | server.spec.e2e.ts # E2E tests using the MCP client 85 | ``` 86 | 87 | This way, tests stay close to the code they're testing, making it easier to: 88 | - Find tests when working on a feature 89 | - Understand the relationship between tests and code 90 | - Refactor code and tests together 91 | - Maintain consistency between implementations and tests 92 | 93 | ## The Arrange/Act/Assert Pattern 94 | 95 | All tests should follow the Arrange/Act/Assert pattern: 96 | 97 | ```typescript 98 | test('should do something', () => { 99 | // Arrange - set up the test 100 | const input = 'something'; 101 | 102 | // Act - perform the action being tested 103 | const result = doSomething(input); 104 | 105 | // Assert - check that the action had the expected result 106 | expect(result).toBe('expected output'); 107 | }); 108 | ``` 109 | 110 | ## Running Tests 111 | 112 | - Run all tests: `npm test` 113 | - Run unit tests: `npm run test:unit` 114 | - Run integration tests: `npm run test:int` 115 | - Run E2E tests: `npm run test:e2e` 116 | 117 | ## CI/CD Integration 118 | 119 | Our CI/CD pipeline runs all test levels to ensure code quality: 120 | 121 | 1. Static analysis with TypeScript and ESLint 122 | 2. Unit tests 123 | 3. Integration tests 124 | 4. End-to-end tests 125 | 126 | ## Best Practices 127 | 128 | 1. Focus on integration tests for the bulk of your test coverage 129 | 2. Write unit tests only for complex business logic 130 | 3. Avoid testing implementation details 131 | 4. Use real dependencies when possible rather than mocks 132 | 5. Keep E2E tests focused on critical user flows 133 | 6. Use the arrange/act/assert pattern consistently 134 | 7. Co-locate tests with the code they're testing 135 | 136 | ## References 137 | 138 | - [The Testing Trophy and Testing Classifications](https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications) by Kent C. Dodds 139 | - [Testing of Microservices](https://engineering.atspotify.com/2018/01/testing-of-microservices/) (Testing Honeycomb approach) by Spotify Engineering 140 | - [Feature Sliced Design](https://feature-sliced.design/) for co-location of tests with feature implementations ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # ℹ️ DISCUSSION: [Microsoft launched an official ADO MCP Server! 🎉🎉🎉](https://github.com/Tiberriver256/mcp-server-azure-devops/discussions/237) 2 | 3 | # Azure DevOps MCP Server 4 | 5 | A Model Context Protocol (MCP) server implementation for Azure DevOps, allowing AI assistants to interact with Azure DevOps APIs through a standardized protocol. 6 | 7 | ## Overview 8 | 9 | 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: 10 | 11 | - Access and manage projects, work items, repositories, and more 12 | - Create and update work items, branches, and pull requests 13 | - Execute common DevOps workflows through natural language 14 | - Access repository content via standardized resource URIs 15 | - Safely authenticate and interact with Azure DevOps resources 16 | 17 | ## Server Structure 18 | 19 | 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: 20 | 21 | - Projects 22 | - Work Items 23 | - Repositories 24 | - Pull Requests 25 | - Branches 26 | - Pipelines 27 | 28 | ### Core Components 29 | 30 | - **AzureDevOpsServer**: Main server class that initializes the MCP server and registers tools 31 | - **Feature Modules**: Organized by feature area (work-items, projects, repositories, etc.) 32 | - **Request Handlers**: Each feature module provides request identification and handling functions 33 | - **Tool Handlers**: Modular functions for each Azure DevOps operation 34 | - **Configuration**: Environment-based configuration for organization URL, PAT, etc. 35 | 36 | 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. 37 | 38 | ## Getting Started 39 | 40 | ### Prerequisites 41 | 42 | - Node.js (v16+) 43 | - npm or yarn 44 | - Azure DevOps account with appropriate access 45 | - Authentication credentials (see [Authentication Guide](docs/authentication.md) for details): 46 | - Personal Access Token (PAT), or 47 | - Azure Identity credentials, or 48 | - Azure CLI login 49 | 50 | ### Running with NPX 51 | 52 | ### Usage with Claude Desktop/Cursor AI 53 | 54 | To integrate with Claude Desktop or Cursor AI, add one of the following configurations to your configuration file. 55 | 56 | #### Azure Identity Authentication 57 | 58 | Be sure you are logged in to Azure CLI with `az login` then add the following: 59 | 60 | ```json 61 | { 62 | "mcpServers": { 63 | "azureDevOps": { 64 | "command": "npx", 65 | "args": ["-y", "@tiberriver256/mcp-server-azure-devops"], 66 | "env": { 67 | "AZURE_DEVOPS_ORG_URL": "https://dev.azure.com/your-organization", 68 | "AZURE_DEVOPS_AUTH_METHOD": "azure-identity", 69 | "AZURE_DEVOPS_DEFAULT_PROJECT": "your-project-name" 70 | } 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | #### Personal Access Token (PAT) Authentication 77 | 78 | ```json 79 | { 80 | "mcpServers": { 81 | "azureDevOps": { 82 | "command": "npx", 83 | "args": ["-y", "@tiberriver256/mcp-server-azure-devops"], 84 | "env": { 85 | "AZURE_DEVOPS_ORG_URL": "https://dev.azure.com/your-organization", 86 | "AZURE_DEVOPS_AUTH_METHOD": "pat", 87 | "AZURE_DEVOPS_PAT": "<YOUR_PAT>", 88 | "AZURE_DEVOPS_DEFAULT_PROJECT": "your-project-name" 89 | } 90 | } 91 | } 92 | } 93 | ``` 94 | 95 | For detailed configuration instructions and more authentication options, see the [Authentication Guide](docs/authentication.md). 96 | 97 | ## Authentication Methods 98 | 99 | 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](docs/authentication.md). 100 | 101 | ### Supported Authentication Methods 102 | 103 | 1. **Personal Access Token (PAT)** - Simple token-based authentication 104 | 2. **Azure Identity (DefaultAzureCredential)** - Flexible authentication using the Azure Identity SDK 105 | 3. **Azure CLI** - Authentication using your Azure CLI login 106 | 107 | Example configuration files for each authentication method are available in the [examples directory](docs/examples/). 108 | 109 | ## Environment Variables 110 | 111 | For a complete list of environment variables and their descriptions, see the [Authentication Guide](docs/authentication.md#configuration-reference). 112 | 113 | Key environment variables include: 114 | 115 | | Variable | Description | Required | Default | 116 | | ------------------------------ | ---------------------------------------------------------------------------------- | ---------------------------- | ---------------- | 117 | | `AZURE_DEVOPS_AUTH_METHOD` | Authentication method (`pat`, `azure-identity`, or `azure-cli`) - case-insensitive | No | `azure-identity` | 118 | | `AZURE_DEVOPS_ORG_URL` | Full URL to your Azure DevOps organization | Yes | - | 119 | | `AZURE_DEVOPS_PAT` | Personal Access Token (for PAT auth) | Only with PAT auth | - | 120 | | `AZURE_DEVOPS_DEFAULT_PROJECT` | Default project if none specified | No | - | 121 | | `AZURE_DEVOPS_API_VERSION` | API version to use | No | Latest | 122 | | `AZURE_TENANT_ID` | Azure AD tenant ID (for service principals) | Only with service principals | - | 123 | | `AZURE_CLIENT_ID` | Azure AD application ID (for service principals) | Only with service principals | - | 124 | | `AZURE_CLIENT_SECRET` | Azure AD client secret (for service principals) | Only with service principals | - | 125 | | `LOG_LEVEL` | Logging level (debug, info, warn, error) | No | info | 126 | 127 | ## Troubleshooting Authentication 128 | 129 | For detailed troubleshooting information for each authentication method, see the [Authentication Guide](docs/authentication.md#troubleshooting-authentication-issues). 130 | 131 | Common issues include: 132 | 133 | - Invalid or expired credentials 134 | - Insufficient permissions 135 | - Network connectivity problems 136 | - Configuration errors 137 | 138 | ## Authentication Implementation Details 139 | 140 | For technical details about how authentication is implemented in the Azure DevOps MCP server, see the [Authentication Guide](docs/authentication.md) and the source code in the `src/auth` directory. 141 | 142 | ## Available Tools 143 | 144 | 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. 145 | 146 | ### User Tools 147 | 148 | - `get_me`: Get details of the authenticated user (id, displayName, email) 149 | 150 | ### Organization Tools 151 | 152 | - `list_organizations`: List all accessible organizations 153 | 154 | ### Project Tools 155 | 156 | - `list_projects`: List all projects in an organization 157 | - `get_project`: Get details of a specific project 158 | - `get_project_details`: Get comprehensive details of a project including process, work item types, and teams 159 | 160 | ### Repository Tools 161 | 162 | - `list_repositories`: List all repositories in a project 163 | - `get_repository`: Get details of a specific repository 164 | - `get_repository_details`: Get detailed information about a repository including statistics and refs 165 | - `get_file_content`: Get content of a file or directory from a repository 166 | 167 | ### Work Item Tools 168 | 169 | - `get_work_item`: Retrieve a work item by ID 170 | - `create_work_item`: Create a new work item 171 | - `update_work_item`: Update an existing work item 172 | - `list_work_items`: List work items in a project 173 | - `manage_work_item_link`: Add, remove, or update links between work items 174 | 175 | ### Search Tools 176 | 177 | - `search_code`: Search for code across repositories in a project 178 | - `search_wiki`: Search for content across wiki pages in a project 179 | - `search_work_items`: Search for work items across projects in Azure DevOps 180 | 181 | ### Pipelines Tools 182 | 183 | - `list_pipelines`: List pipelines in a project 184 | - `get_pipeline`: Get details of a specific pipeline 185 | - `trigger_pipeline`: Trigger a pipeline run with customizable parameters 186 | 187 | ### Wiki Tools 188 | 189 | - `get_wikis`: List all wikis in a project 190 | - `get_wiki_page`: Get content of a specific wiki page as plain text 191 | 192 | ### Pull Request Tools 193 | 194 | - [`create_pull_request`](docs/tools/pull-requests.md#create_pull_request) - Create a new pull request 195 | - [`list_pull_requests`](docs/tools/pull-requests.md#list_pull_requests) - List pull requests in a repository 196 | - [`add_pull_request_comment`](docs/tools/pull-requests.md#add_pull_request_comment) - Add a comment to a pull request 197 | - [`get_pull_request_comments`](docs/tools/pull-requests.md#get_pull_request_comments) - Get comments from a pull request 198 | - [`update_pull_request`](docs/tools/pull-requests.md#update_pull_request) - Update an existing pull request (title, description, status, draft state, reviewers, work items) 199 | 200 | For comprehensive documentation on all tools, see the [Tools Documentation](docs/tools/). 201 | 202 | ## Contributing 203 | 204 | Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. 205 | 206 | ## Star History 207 | 208 | [](https://www.star-history.com/#tiberriver256/mcp-server-azure-devops&Date) 209 | 210 | ## License 211 | 212 | MIT 213 | ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown 1 | # Contributing to Azure DevOps MCP Server 2 | 3 | We love your input! We want to make contributing to Azure DevOps MCP Server as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## Development Process 12 | 13 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 14 | 15 | ## Pull Requests 16 | 17 | 1. Fork the repository 18 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 19 | 3. Commit your changes (see Commit Message Guidelines below) 20 | 4. Push to the branch (`git push origin feature/amazing-feature`) 21 | 5. Open a Pull Request 22 | 23 | ## Development Practices 24 | 25 | This project follows Test-Driven Development practices. Each new feature should: 26 | 27 | 1. Begin with a failing test 28 | 2. Implement the minimal code to make the test pass 29 | 3. Refactor while keeping tests green 30 | 31 | ## Project Structure 32 | 33 | The server is organized into feature-specific modules: 34 | 35 | ``` 36 | src/ 37 | └── features/ 38 | ├── organizations/ 39 | ├── pipelines/ 40 | ├── projects/ 41 | ├── pull-requests/ 42 | ├── repositories/ 43 | ├── search/ 44 | ├── users/ 45 | ├── wikis/ 46 | └── work-items/ 47 | ``` 48 | 49 | Each feature module: 50 | 1. Exports its schemas, types, and individual tool functions 51 | 2. Provides an `is<FeatureName>Request` function to identify if a request is for this feature 52 | 3. Provides a `handle<FeatureName>Request` function to handle requests for this feature 53 | 54 | ### Adding a New Feature or Tool 55 | 56 | When adding a new feature or tool: 57 | 1. Create a new directory under `src/features/` if needed 58 | 2. Implement your tool functions 59 | 3. Update the feature's `index.ts` to export your functions and add them to the request handlers 60 | 4. No changes to server.ts should be needed! 61 | 62 | ## Testing 63 | 64 | ### Unit Tests 65 | 66 | Run unit tests with: 67 | 68 | ```bash 69 | npm run test:unit 70 | ``` 71 | 72 | ### Integration Tests 73 | 74 | Integration tests require a connection to a real Azure DevOps instance. To run them: 75 | 76 | 1. Ensure your `.env` file is configured with valid Azure DevOps credentials: 77 | 78 | ``` 79 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization 80 | AZURE_DEVOPS_PAT=your-personal-access-token 81 | AZURE_DEVOPS_DEFAULT_PROJECT=your-project-name 82 | ``` 83 | 84 | 2. Run the integration tests: 85 | ```bash 86 | npm run test:integration 87 | ``` 88 | 89 | ### CI Environment 90 | 91 | For running tests in CI environments (like GitHub Actions), see [CI Environment Setup](docs/ci-setup.md) for instructions on configuring secrets. 92 | 93 | ## Commit Message Guidelines 94 | 95 | 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. 96 | 97 | ### Commit Message Format 98 | 99 | 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**: 100 | 101 | ``` 102 | <type>(<scope>): <subject> 103 | <BLANK LINE> 104 | <body> 105 | <BLANK LINE> 106 | <footer> 107 | ``` 108 | 109 | The **header** is mandatory, while the **scope** of the header is optional. 110 | 111 | ### Type 112 | 113 | Must be one of the following: 114 | 115 | - **feat**: A new feature 116 | - **fix**: A bug fix 117 | - **docs**: Documentation only changes 118 | - **style**: Changes that do not affect the meaning of the code (white-space, formatting, etc) 119 | - **refactor**: A code change that neither fixes a bug nor adds a feature 120 | - **perf**: A code change that improves performance 121 | - **test**: Adding missing tests or correcting existing tests 122 | - **build**: Changes that affect the build system or external dependencies 123 | - **ci**: Changes to our CI configuration files and scripts 124 | - **chore**: Other changes that don't modify src or test files 125 | 126 | ### Subject 127 | 128 | The subject contains a succinct description of the change: 129 | 130 | - Use the imperative, present tense: "change" not "changed" nor "changes" 131 | - Don't capitalize the first letter 132 | - No period (.) at the end 133 | 134 | ### Body 135 | 136 | The body should include the motivation for the change and contrast this with previous behavior. 137 | 138 | ### Footer 139 | 140 | The footer should contain any information about **Breaking Changes** and is also the place to reference GitHub issues that this commit closes. 141 | 142 | 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. 143 | 144 | ### Using the Interactive Tool 145 | 146 | 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: 147 | 148 | ```bash 149 | npm run commit 150 | ``` 151 | 152 | This will start an interactive prompt that will help you generate a properly formatted commit message. 153 | 154 | ## Release Process 155 | 156 | This project uses [Conventional Commits](https://www.conventionalcommits.org/) to automate versioning and changelog generation. When contributing, please follow the commit message convention. 157 | 158 | To create a commit with the correct format, use: 159 | ```bash 160 | npm run commit 161 | ``` 162 | 163 | ## Automated Release Workflow 164 | 165 | 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. 166 | 167 | The workflow is automatically triggered on pushes to the `main` branch and follows this process: 168 | 169 | 1. Release Please analyzes commit messages since the last release 170 | 2. If releasable changes are detected, it creates or updates a Release PR 171 | 3. When the Release PR is merged, it: 172 | - Updates the version in package.json 173 | - Updates CHANGELOG.md with details of all changes 174 | - Creates a Git tag and GitHub Release 175 | - Publishes the package to npm 176 | 177 | ### Release PR Process 178 | 179 | 1. When commits with conventional commit messages are pushed to `main`, Release Please automatically creates a Release PR 180 | 2. The Release PR contains all the changes since the last release with proper version bump based on commit types: 181 | - `feat:` commits trigger a minor version bump 182 | - `fix:` commits trigger a patch version bump 183 | - `feat!:` or `fix!:` commits with breaking changes trigger a major version bump 184 | 3. Review the Release PR to ensure the changelog and version bump are correct 185 | 4. Merge the Release PR to trigger the actual release 186 | 187 | This automation ensures consistent and well-documented releases that accurately reflect the changes made since the previous release. 188 | 189 | ## License 190 | 191 | By contributing, you agree that your contributions will be licensed under the project's license. ``` -------------------------------------------------------------------------------- /.github/release-please-manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | ".": "0.1.42" 3 | } ``` -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- ```yaml 1 | github: Tiberriver256 2 | ``` -------------------------------------------------------------------------------- /src/shared/api/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './client'; 2 | ``` -------------------------------------------------------------------------------- /src/shared/types/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './config'; 2 | ``` -------------------------------------------------------------------------------- /src/features/pull-requests/add-pull-request-comment/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './feature'; 2 | ``` -------------------------------------------------------------------------------- /src/features/pull-requests/get-pull-request-comments/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './feature'; 2 | ``` -------------------------------------------------------------------------------- /src/features/pull-requests/update-pull-request/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './feature'; 2 | ``` -------------------------------------------------------------------------------- /src/features/search/search-wiki/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './feature'; 2 | ``` -------------------------------------------------------------------------------- /src/features/search/search-work-items/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './feature'; 2 | ``` -------------------------------------------------------------------------------- /src/shared/config/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './version'; 2 | ``` -------------------------------------------------------------------------------- /src/shared/errors/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './azure-devops-errors'; 2 | ``` -------------------------------------------------------------------------------- /src/features/organizations/list-organizations/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schema'; 2 | export * from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/pipelines/get-pipeline/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './feature'; 2 | export * from './schema'; 3 | ``` -------------------------------------------------------------------------------- /src/features/pipelines/list-pipelines/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './feature'; 2 | export * from './schema'; 3 | ``` -------------------------------------------------------------------------------- /src/features/projects/get-project/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schema'; 2 | export * from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/projects/list-projects/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schema'; 2 | export * from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/pull-requests/create-pull-request/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schema'; 2 | export * from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/pull-requests/list-pull-requests/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './feature'; 2 | export * from './schema'; 3 | ``` -------------------------------------------------------------------------------- /src/features/pull-requests/list-pull-requests/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { ListPullRequestsSchema } from '../schemas'; 2 | ``` -------------------------------------------------------------------------------- /src/features/repositories/get-all-repositories-tree/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schema'; 2 | export * from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/repositories/get-file-content/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './feature'; 2 | export * from './schema'; 3 | ``` -------------------------------------------------------------------------------- /src/features/repositories/get-repository-details/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schema'; 2 | export * from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/repositories/get-repository/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schema'; 2 | export * from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/repositories/list-repositories/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schema'; 2 | export * from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/users/get-me/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schema'; 2 | export * from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/work-items/create-work-item/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schema'; 2 | export * from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/work-items/get-work-item/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schema'; 2 | export * from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/work-items/list-work-items/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schema'; 2 | export * from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/work-items/update-work-item/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schema'; 2 | export * from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/search/search-code/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './feature'; 2 | export * from '../schemas'; 3 | ``` -------------------------------------------------------------------------------- /src/features/users/get-me/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { GetMeSchema } from '../schemas'; 2 | 3 | export { GetMeSchema }; 4 | ``` -------------------------------------------------------------------------------- /src/features/projects/get-project/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { GetProjectSchema } from '../schemas'; 2 | 3 | export { GetProjectSchema }; 4 | ``` -------------------------------------------------------------------------------- /src/features/work-items/get-work-item/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { GetWorkItemSchema } from '../schemas'; 2 | 3 | export { GetWorkItemSchema }; 4 | ``` -------------------------------------------------------------------------------- /src/features/projects/list-projects/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListProjectsSchema } from '../schemas'; 2 | 3 | export { ListProjectsSchema }; 4 | ``` -------------------------------------------------------------------------------- /src/features/wikis/get-wikis/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { getWikis } from './feature'; 2 | export { GetWikisSchema } from './schema'; 3 | ``` -------------------------------------------------------------------------------- /src/features/repositories/get-repository/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { GetRepositorySchema } from '../schemas'; 2 | 3 | export { GetRepositorySchema }; 4 | ``` -------------------------------------------------------------------------------- /src/features/work-items/list-work-items/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListWorkItemsSchema } from '../schemas'; 2 | 3 | export { ListWorkItemsSchema }; 4 | ``` -------------------------------------------------------------------------------- /src/features/work-items/create-work-item/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CreateWorkItemSchema } from '../schemas'; 2 | 3 | export { CreateWorkItemSchema }; 4 | ``` -------------------------------------------------------------------------------- /src/features/work-items/update-work-item/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { UpdateWorkItemSchema } from '../schemas'; 2 | 3 | export { UpdateWorkItemSchema }; 4 | ``` -------------------------------------------------------------------------------- /src/features/wikis/get-wiki-page/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { getWikiPage } from './feature'; 2 | export { GetWikiPageSchema } from './schema'; 3 | ``` -------------------------------------------------------------------------------- /src/features/repositories/list-repositories/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListRepositoriesSchema } from '../schemas'; 2 | 3 | export { ListRepositoriesSchema }; 4 | ``` -------------------------------------------------------------------------------- /src/shared/config/version.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Current version of the Azure DevOps MCP server 3 | */ 4 | export const VERSION = '0.1.0'; 5 | ``` -------------------------------------------------------------------------------- /src/features/organizations/list-organizations/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ListOrganizationsSchema } from '../schemas'; 2 | 3 | export { ListOrganizationsSchema }; 4 | ``` -------------------------------------------------------------------------------- /src/features/projects/get-project-details/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { GetProjectDetailsSchema } from '../schemas'; 2 | 3 | export { GetProjectDetailsSchema }; 4 | ``` -------------------------------------------------------------------------------- /src/features/pull-requests/create-pull-request/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CreatePullRequestSchema } from '../schemas'; 2 | 3 | export { CreatePullRequestSchema }; 4 | ``` -------------------------------------------------------------------------------- /src/features/wikis/list-wiki-pages/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { ListWikiPagesSchema } from './schema'; 2 | export { listWikiPages } from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/wikis/create-wiki-page/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { createWikiPage } from './feature'; 2 | export { CreateWikiPageSchema } from './schema'; 3 | ``` -------------------------------------------------------------------------------- /src/features/work-items/manage-work-item-link/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ManageWorkItemLinkSchema } from '../schemas'; 2 | 3 | export { ManageWorkItemLinkSchema }; 4 | ``` -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- ```javascript 1 | // commitlint.config.js 2 | module.exports = { 3 | extends: ['@commitlint/config-conventional'], 4 | }; ``` -------------------------------------------------------------------------------- /src/features/pipelines/trigger-pipeline/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { triggerPipeline } from './feature'; 2 | export { TriggerPipelineSchema } from './schema'; 3 | ``` -------------------------------------------------------------------------------- /src/features/wikis/create-wiki/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { createWiki } from './feature'; 2 | export { CreateWikiSchema, WikiType } from './schema'; 3 | ``` -------------------------------------------------------------------------------- /src/features/repositories/get-repository-details/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { GetRepositoryDetailsSchema } from '../schemas'; 2 | 3 | export { GetRepositoryDetailsSchema }; 4 | ``` -------------------------------------------------------------------------------- /src/features/projects/get-project-details/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { GetProjectDetailsSchema } from './schema'; 2 | export { getProjectDetails } from './feature'; 3 | ``` -------------------------------------------------------------------------------- /src/features/work-items/manage-work-item-link/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { manageWorkItemLink } from './feature'; 2 | export { ManageWorkItemLinkSchema } from './schema'; 3 | ``` -------------------------------------------------------------------------------- /project-management/planning/project-plan.md: -------------------------------------------------------------------------------- ```markdown 1 | # Project Plan 2 | 3 | The project plan has been moved to: 4 | 5 | https://github.com/users/Tiberriver256/projects/1 6 | ``` -------------------------------------------------------------------------------- /src/features/wikis/update-wiki-page/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { updateWikiPage, UpdateWikiPageOptions } from './feature'; 2 | export { UpdateWikiPageSchema } from './schema'; 3 | ``` -------------------------------------------------------------------------------- /src/features/repositories/get-file-content/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { GetFileContentSchema } from '../schemas'; 2 | 3 | // Export with explicit name to avoid conflicts 4 | export { GetFileContentSchema }; 5 | ``` -------------------------------------------------------------------------------- /src/features/users/schemas.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | /** 4 | * Schema for the get_me tool, which takes no parameters 5 | */ 6 | export const GetMeSchema = z.object({}).strict(); 7 | ``` -------------------------------------------------------------------------------- /src/features/repositories/get-all-repositories-tree/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { GetAllRepositoriesTreeSchema } from '../schemas'; 2 | 3 | // Export with explicit name to avoid conflicts 4 | export { GetAllRepositoriesTreeSchema }; 5 | ``` -------------------------------------------------------------------------------- /src/features/organizations/schemas.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | /** 4 | * Schema for the list organizations request 5 | * Note: This is an empty schema because the operation doesn't require any parameters 6 | */ 7 | export const ListOrganizationsSchema = z.object({}); 8 | ``` -------------------------------------------------------------------------------- /src/shared/types/tool-definition.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { JsonSchema7Type } from 'zod-to-json-schema'; 2 | 3 | /** 4 | * Represents a tool that can be listed in the ListTools response 5 | */ 6 | export interface ToolDefinition { 7 | name: string; 8 | description: string; 9 | inputSchema: JsonSchema7Type; 10 | } 11 | ``` -------------------------------------------------------------------------------- /project-management/tdd-cycle.xml: -------------------------------------------------------------------------------- ``` 1 | <tdd-cycle> 2 | <red>Write a failing test for new functionality.</red> 3 | <green>Implement minimal code to pass the test.</green> 4 | <refactor>Refactor code, ensuring tests pass.</refactor> 5 | <repeat>Repeat for each new test.</repeat> 6 | </tdd-cycle> ``` -------------------------------------------------------------------------------- /src/features/users/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * User profile information 3 | */ 4 | export interface UserProfile { 5 | /** 6 | * The ID of the user 7 | */ 8 | id: string; 9 | 10 | /** 11 | * The display name of the user 12 | */ 13 | displayName: string; 14 | 15 | /** 16 | * The email address of the user 17 | */ 18 | email: string; 19 | } 20 | ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "explorer.fileNesting.enabled": true, 3 | "explorer.fileNesting.patterns": { 4 | "*.ts": "${capture}.spec.unit.ts, ${capture}.spec.int.ts, ${capture}.spec.e2e.ts" 5 | }, 6 | "typescript.preferences.importModuleSpecifier": "non-relative", 7 | "typescript.updateImportsOnFileMove.enabled": "always" 8 | } ``` -------------------------------------------------------------------------------- /src/features/projects/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { TeamProject } from 'azure-devops-node-api/interfaces/CoreInterfaces'; 2 | 3 | /** 4 | * Options for listing projects 5 | */ 6 | export interface ListProjectsOptions { 7 | stateFilter?: number; 8 | top?: number; 9 | skip?: number; 10 | continuationToken?: number; 11 | } 12 | 13 | // Re-export TeamProject type for convenience 14 | export type { TeamProject }; 15 | ``` -------------------------------------------------------------------------------- /src/features/organizations/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Organization interface 3 | */ 4 | export interface Organization { 5 | /** 6 | * The ID of the organization 7 | */ 8 | id: string; 9 | 10 | /** 11 | * The name of the organization 12 | */ 13 | name: string; 14 | 15 | /** 16 | * The URL of the organization 17 | */ 18 | url: string; 19 | } 20 | 21 | /** 22 | * Azure DevOps resource ID for token acquisition 23 | */ 24 | export const AZURE_DEVOPS_RESOURCE_ID = '499b84ac-1321-427f-aa17-267ca6975798'; 25 | ``` -------------------------------------------------------------------------------- /jest.int.config.js: -------------------------------------------------------------------------------- ```javascript 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | roots: ['<rootDir>/src'], 6 | testMatch: ['**/*.spec.int.ts'], 7 | moduleNameMapper: { 8 | '^@/(.*)$': '<rootDir>/src/$1', 9 | }, 10 | collectCoverage: false, 11 | verbose: true, 12 | testPathIgnorePatterns: ['/node_modules/', '/dist/'], 13 | setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'], 14 | }; ``` -------------------------------------------------------------------------------- /jest.unit.config.js: -------------------------------------------------------------------------------- ```javascript 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | roots: ['<rootDir>/src'], 6 | testMatch: ['**/*.spec.unit.ts'], 7 | moduleNameMapper: { 8 | '^@/(.*)$': '<rootDir>/src/$1', 9 | }, 10 | collectCoverage: false, 11 | verbose: true, 12 | testPathIgnorePatterns: ['/node_modules/', '/dist/'], 13 | setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'], 14 | }; ``` -------------------------------------------------------------------------------- /.github/release-please-config.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "packages": { 3 | ".": { 4 | "release-type": "node", 5 | "package-name": "@tiberriver256/mcp-server-azure-devops", 6 | "changelog-path": "CHANGELOG.md", 7 | "bump-minor-pre-major": true, 8 | "bump-patch-for-minor-pre-major": true, 9 | "draft": false, 10 | "prerelease": false 11 | } 12 | }, 13 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 14 | } ``` -------------------------------------------------------------------------------- /src/features/users/tool-definitions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { zodToJsonSchema } from 'zod-to-json-schema'; 2 | import { ToolDefinition } from '../../shared/types/tool-definition'; 3 | import { GetMeSchema } from './schemas'; 4 | 5 | /** 6 | * List of users tools 7 | */ 8 | export const usersTools: ToolDefinition[] = [ 9 | { 10 | name: 'get_me', 11 | description: 12 | 'Get details of the authenticated user (id, displayName, email)', 13 | inputSchema: zodToJsonSchema(GetMeSchema), 14 | }, 15 | ]; 16 | ``` -------------------------------------------------------------------------------- /src/shared/auth/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Authentication module for Azure DevOps 3 | * 4 | * This module provides authentication functionality for Azure DevOps API. 5 | * It supports multiple authentication methods: 6 | * - Personal Access Token (PAT) 7 | * - Azure Identity (DefaultAzureCredential) 8 | * - Azure CLI (AzureCliCredential) 9 | */ 10 | 11 | export { 12 | AuthenticationMethod, 13 | AuthConfig, 14 | createAuthClient, 15 | } from './auth-factory'; 16 | export { AzureDevOpsClient } from './client-factory'; 17 | ``` -------------------------------------------------------------------------------- /src/features/organizations/tool-definitions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { zodToJsonSchema } from 'zod-to-json-schema'; 2 | import { ToolDefinition } from '../../shared/types/tool-definition'; 3 | import { ListOrganizationsSchema } from './schemas'; 4 | 5 | /** 6 | * List of organizations tools 7 | */ 8 | export const organizationsTools: ToolDefinition[] = [ 9 | { 10 | name: 'list_organizations', 11 | description: 12 | 'List all Azure DevOps organizations accessible to the current authentication', 13 | inputSchema: zodToJsonSchema(ListOrganizationsSchema), 14 | }, 15 | ]; 16 | ``` -------------------------------------------------------------------------------- /src/features/wikis/get-wikis/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | import { defaultProject, defaultOrg } from '../../../utils/environment'; 4 | 5 | /** 6 | * Schema for listing wikis in an Azure DevOps project or organization 7 | */ 8 | export const GetWikisSchema = z.object({ 9 | organizationId: z 10 | .string() 11 | .optional() 12 | .nullable() 13 | .describe(`The ID or name of the organization (Default: ${defaultOrg})`), 14 | projectId: z 15 | .string() 16 | .optional() 17 | .nullable() 18 | .describe(`The ID or name of the project (Default: ${defaultProject})`), 19 | }); 20 | ``` -------------------------------------------------------------------------------- /jest.e2e.config.js: -------------------------------------------------------------------------------- ```javascript 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | roots: ['<rootDir>/src'], 6 | testMatch: ['**/*.spec.e2e.ts'], 7 | moduleNameMapper: { 8 | '^@/(.*)$': '<rootDir>/src/$1', 9 | }, 10 | collectCoverage: false, 11 | verbose: true, 12 | testPathIgnorePatterns: ['/node_modules/', '/dist/'], 13 | setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'], 14 | testTimeout: 30000, // Longer timeout for E2E tests 15 | passWithNoTests: true, // Allow tests to pass when no tests exist yet 16 | }; ``` -------------------------------------------------------------------------------- /.kilocode/mcp.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "mcpServers": { 3 | "shrimp-task-manager": { 4 | "command": "npx", 5 | "args": [ 6 | "-y", 7 | "mcp-shrimp-task-manager" 8 | ], 9 | "env": { 10 | "DATA_DIR": "D:/mcp-server-azure-devops", 11 | "TEMPLATES_USE": "en", 12 | "ENABLE_GUI": "false" 13 | }, 14 | "alwaysAllow": [ 15 | "init_project_rules", 16 | "process_thought", 17 | "plan_task", 18 | "analyze_task", 19 | "reflect_task", 20 | "split_tasks", 21 | "execute_task", 22 | "verify_task", 23 | "get_task_detail" 24 | ] 25 | } 26 | } 27 | } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "outDir": "dist", 12 | "sourceMap": true, 13 | "declaration": true, 14 | "strictNullChecks": true, 15 | "noImplicitAny": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": ["src/*"] 21 | } 22 | }, 23 | "include": ["src/**/*"], 24 | "exclude": ["node_modules", "dist"] 25 | } 26 | ``` -------------------------------------------------------------------------------- /src/features/pipelines/list-pipelines/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { defaultProject } from '../../../utils/environment'; 3 | 4 | /** 5 | * Schema for the listPipelines function 6 | */ 7 | export const ListPipelinesSchema = z.object({ 8 | // The project to list pipelines from 9 | projectId: z 10 | .string() 11 | .optional() 12 | .describe(`The ID or name of the project (Default: ${defaultProject})`), 13 | // Maximum number of pipelines to return 14 | top: z.number().optional().describe('Maximum number of pipelines to return'), 15 | // Order by field and direction 16 | orderBy: z 17 | .string() 18 | .optional() 19 | .describe('Order by field and direction (e.g., "createdDate desc")'), 20 | }); 21 | ``` -------------------------------------------------------------------------------- /src/features/wikis/get-wiki-page/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | import { defaultProject, defaultOrg } from '../../../utils/environment'; 4 | 5 | /** 6 | * Schema for getting a wiki page from an Azure DevOps wiki 7 | */ 8 | export const GetWikiPageSchema = z.object({ 9 | organizationId: z 10 | .string() 11 | .optional() 12 | .nullable() 13 | .describe(`The ID or name of the organization (Default: ${defaultOrg})`), 14 | projectId: z 15 | .string() 16 | .optional() 17 | .nullable() 18 | .describe(`The ID or name of the project (Default: ${defaultProject})`), 19 | wikiId: z.string().describe('The ID or name of the wiki'), 20 | pagePath: z.string().describe('The path of the page within the wiki'), 21 | }); 22 | ``` -------------------------------------------------------------------------------- /src/features/wikis/list-wiki-pages/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | import { defaultProject, defaultOrg } from '../../../utils/environment'; 4 | 5 | /** 6 | * Schema for listing wiki pages from an Azure DevOps wiki 7 | */ 8 | export const ListWikiPagesSchema = z.object({ 9 | organizationId: z 10 | .string() 11 | .optional() 12 | .nullable() 13 | .describe(`The ID or name of the organization (Default: ${defaultOrg})`), 14 | projectId: z 15 | .string() 16 | .optional() 17 | .nullable() 18 | .describe(`The ID or name of the project (Default: ${defaultProject})`), 19 | wikiId: z.string().describe('The ID or name of the wiki'), 20 | }); 21 | 22 | export type ListWikiPagesOptions = z.infer<typeof ListWikiPagesSchema>; 23 | ``` -------------------------------------------------------------------------------- /src/shared/types/request-handler.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | CallToolRequest, 3 | CallToolResult, 4 | } from '@modelcontextprotocol/sdk/types.js'; 5 | import { WebApi } from 'azure-devops-node-api'; 6 | 7 | /** 8 | * Function type for identifying if a request belongs to a specific feature. 9 | */ 10 | export interface RequestIdentifier { 11 | (request: CallToolRequest): boolean; 12 | } 13 | 14 | /** 15 | * Function type for handling feature-specific requests. 16 | * Returns either the standard MCP CallToolResult or a simplified response structure 17 | * for backward compatibility. 18 | */ 19 | export interface RequestHandler { 20 | ( 21 | connection: WebApi, 22 | request: CallToolRequest, 23 | ): Promise< 24 | CallToolResult | { content: Array<{ type: string; text: string }> } 25 | >; 26 | } 27 | ``` -------------------------------------------------------------------------------- /src/features/pipelines/get-pipeline/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { defaultProject } from '../../../utils/environment'; 3 | 4 | /** 5 | * Schema for the getPipeline function 6 | */ 7 | export const GetPipelineSchema = z.object({ 8 | // The project containing the pipeline 9 | projectId: z 10 | .string() 11 | .optional() 12 | .describe(`The ID or name of the project (Default: ${defaultProject})`), 13 | // The ID of the pipeline to retrieve 14 | pipelineId: z 15 | .number() 16 | .int() 17 | .positive() 18 | .describe('The numeric ID of the pipeline to retrieve'), 19 | // The version of the pipeline to retrieve 20 | pipelineVersion: z 21 | .number() 22 | .int() 23 | .positive() 24 | .optional() 25 | .describe( 26 | 'The version of the pipeline to retrieve (latest if not specified)', 27 | ), 28 | }); 29 | ``` -------------------------------------------------------------------------------- /src/shared/types/config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { AuthenticationMethod } from '../auth/auth-factory'; 2 | 3 | /** 4 | * Azure DevOps configuration type definition 5 | */ 6 | export interface AzureDevOpsConfig { 7 | /** 8 | * The Azure DevOps organization URL (e.g., https://dev.azure.com/organization) 9 | */ 10 | organizationUrl: string; 11 | 12 | /** 13 | * Authentication method to use (pat, azure-identity, azure-cli) 14 | * @default 'azure-identity' 15 | */ 16 | authMethod?: AuthenticationMethod; 17 | 18 | /** 19 | * Personal Access Token for authentication (required for PAT authentication) 20 | */ 21 | personalAccessToken?: string; 22 | 23 | /** 24 | * Optional default project to use when not specified 25 | */ 26 | defaultProject?: string; 27 | 28 | /** 29 | * Optional API version to use (defaults to latest) 30 | */ 31 | apiVersion?: string; 32 | } 33 | ``` -------------------------------------------------------------------------------- /src/features/search/tool-definitions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { zodToJsonSchema } from 'zod-to-json-schema'; 2 | import { ToolDefinition } from '../../shared/types/tool-definition'; 3 | import { 4 | SearchCodeSchema, 5 | SearchWikiSchema, 6 | SearchWorkItemsSchema, 7 | } from './schemas'; 8 | 9 | /** 10 | * List of search tools 11 | */ 12 | export const searchTools: ToolDefinition[] = [ 13 | { 14 | name: 'search_code', 15 | description: 'Search for code across repositories in a project', 16 | inputSchema: zodToJsonSchema(SearchCodeSchema), 17 | }, 18 | { 19 | name: 'search_wiki', 20 | description: 'Search for content across wiki pages in a project', 21 | inputSchema: zodToJsonSchema(SearchWikiSchema), 22 | }, 23 | { 24 | name: 'search_work_items', 25 | description: 'Search for work items across projects in Azure DevOps', 26 | inputSchema: zodToJsonSchema(SearchWorkItemsSchema), 27 | }, 28 | ]; 29 | ``` -------------------------------------------------------------------------------- /src/features/projects/tool-definitions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { zodToJsonSchema } from 'zod-to-json-schema'; 2 | import { ToolDefinition } from '../../shared/types/tool-definition'; 3 | import { 4 | ListProjectsSchema, 5 | GetProjectSchema, 6 | GetProjectDetailsSchema, 7 | } from './schemas'; 8 | 9 | /** 10 | * List of projects tools 11 | */ 12 | export const projectsTools: ToolDefinition[] = [ 13 | { 14 | name: 'list_projects', 15 | description: 'List all projects in an organization', 16 | inputSchema: zodToJsonSchema(ListProjectsSchema), 17 | }, 18 | { 19 | name: 'get_project', 20 | description: 'Get details of a specific project', 21 | inputSchema: zodToJsonSchema(GetProjectSchema), 22 | }, 23 | { 24 | name: 'get_project_details', 25 | description: 26 | 'Get comprehensive details of a project including process, work item types, and teams', 27 | inputSchema: zodToJsonSchema(GetProjectDetailsSchema), 28 | }, 29 | ]; 30 | ``` -------------------------------------------------------------------------------- /docs/examples/pat-authentication.env: -------------------------------------------------------------------------------- ``` 1 | # Example .env file for Personal Access Token (PAT) authentication 2 | # Replace the values with your own 3 | 4 | # Authentication method (required) 5 | AZURE_DEVOPS_AUTH_METHOD=pat 6 | 7 | # Azure DevOps organization URL (required) 8 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization 9 | 10 | # Personal Access Token (required for PAT authentication) 11 | # Create one at: https://dev.azure.com/your-organization/_usersSettings/tokens 12 | AZURE_DEVOPS_PAT=your-personal-access-token 13 | 14 | # Default project to use when not specified (optional) 15 | AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project 16 | 17 | # API Version to use (optional, defaults to latest) 18 | # AZURE_DEVOPS_API_VERSION=6.0 19 | 20 | # Logging Level (optional) 21 | LOG_LEVEL=info 22 | 23 | # Note: This server uses stdio for communication with the MCP client, 24 | # not HTTP. It does not listen on a network port. ``` -------------------------------------------------------------------------------- /src/features/pipelines/tool-definitions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { zodToJsonSchema } from 'zod-to-json-schema'; 2 | import { ToolDefinition } from '../../shared/types/tool-definition'; 3 | import { ListPipelinesSchema } from './list-pipelines/schema'; 4 | import { GetPipelineSchema } from './get-pipeline/schema'; 5 | import { TriggerPipelineSchema } from './trigger-pipeline/schema'; 6 | 7 | /** 8 | * List of pipelines tools 9 | */ 10 | export const pipelinesTools: ToolDefinition[] = [ 11 | { 12 | name: 'list_pipelines', 13 | description: 'List pipelines in a project', 14 | inputSchema: zodToJsonSchema(ListPipelinesSchema), 15 | }, 16 | { 17 | name: 'get_pipeline', 18 | description: 'Get details of a specific pipeline', 19 | inputSchema: zodToJsonSchema(GetPipelineSchema), 20 | }, 21 | { 22 | name: 'trigger_pipeline', 23 | description: 'Trigger a pipeline run', 24 | inputSchema: zodToJsonSchema(TriggerPipelineSchema), 25 | }, 26 | ]; 27 | ``` -------------------------------------------------------------------------------- /docs/tools/user-tools.md: -------------------------------------------------------------------------------- ```markdown 1 | # Azure DevOps User Tools 2 | 3 | This document describes the user-related tools provided by the Azure DevOps MCP server. 4 | 5 | ## get_me 6 | 7 | The `get_me` tool retrieves information about the currently authenticated user. 8 | 9 | ### Input 10 | 11 | This tool doesn't require any input parameters. 12 | 13 | ```json 14 | {} 15 | ``` 16 | 17 | ### Output 18 | 19 | The tool returns the user's profile information including: 20 | - `id`: The unique identifier for the user 21 | - `displayName`: The user's display name 22 | - `email`: The user's email address 23 | 24 | #### Example Response 25 | 26 | ```json 27 | { 28 | "id": "01234567-89ab-cdef-0123-456789abcdef", 29 | "displayName": "John Doe", 30 | "email": "[email protected]" 31 | } 32 | ``` 33 | 34 | ### Error Handling 35 | 36 | The tool may return the following errors: 37 | 38 | - `AzureDevOpsAuthenticationError`: If authentication fails 39 | - `AzureDevOpsError`: For general errors when retrieving user information ``` -------------------------------------------------------------------------------- /src/features/pipelines/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Re-export the Pipeline interface from the Azure DevOps API 2 | import { 3 | Pipeline, 4 | Run, 5 | } from 'azure-devops-node-api/interfaces/PipelinesInterfaces'; 6 | 7 | /** 8 | * Options for listing pipelines 9 | */ 10 | export interface ListPipelinesOptions { 11 | projectId: string; 12 | orderBy?: string; 13 | top?: number; 14 | continuationToken?: string; 15 | } 16 | 17 | /** 18 | * Options for getting a pipeline 19 | */ 20 | export interface GetPipelineOptions { 21 | projectId: string; 22 | organizationId?: string; 23 | pipelineId: number; 24 | pipelineVersion?: number; 25 | } 26 | 27 | /** 28 | * Options for triggering a pipeline 29 | */ 30 | export interface TriggerPipelineOptions { 31 | projectId: string; 32 | pipelineId: number; 33 | branch?: string; 34 | variables?: Record<string, { value: string; isSecret?: boolean }>; 35 | templateParameters?: Record<string, string>; 36 | stagesToSkip?: string[]; 37 | } 38 | 39 | export { Pipeline, Run }; 40 | ``` -------------------------------------------------------------------------------- /src/features/wikis/update-wiki-page/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | import { defaultProject, defaultOrg } from '../../../utils/environment'; 4 | 5 | /** 6 | * Schema for validating wiki page update options 7 | */ 8 | export const UpdateWikiPageSchema = z.object({ 9 | organizationId: z 10 | .string() 11 | .optional() 12 | .nullable() 13 | .describe(`The ID or name of the organization (Default: ${defaultOrg})`), 14 | projectId: z 15 | .string() 16 | .optional() 17 | .nullable() 18 | .describe(`The ID or name of the project (Default: ${defaultProject})`), 19 | wikiId: z.string().min(1).describe('The ID or name of the wiki'), 20 | pagePath: z.string().min(1).describe('Path of the wiki page to update'), 21 | content: z 22 | .string() 23 | .min(1) 24 | .describe('The new content for the wiki page in markdown format'), 25 | comment: z 26 | .string() 27 | .optional() 28 | .nullable() 29 | .describe('Optional comment for the update'), 30 | }); 31 | ``` -------------------------------------------------------------------------------- /src/features/work-items/__test__/fixtures.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WorkItem } from '../types'; 2 | 3 | /** 4 | * Standard work item fixture for tests 5 | */ 6 | export const createWorkItemFixture = ( 7 | id: number, 8 | title: string = 'Test Work Item', 9 | state: string = 'Active', 10 | assignedTo?: string, 11 | ): WorkItem => { 12 | return { 13 | id, 14 | rev: 1, 15 | fields: { 16 | 'System.Id': id, 17 | 'System.Title': title, 18 | 'System.State': state, 19 | ...(assignedTo ? { 'System.AssignedTo': assignedTo } : {}), 20 | }, 21 | url: `https://dev.azure.com/test-org/test-project/_apis/wit/workItems/${id}`, 22 | } as WorkItem; 23 | }; 24 | 25 | /** 26 | * Create a collection of work items for list tests 27 | */ 28 | export const createWorkItemsFixture = (count: number = 3): WorkItem[] => { 29 | return Array.from({ length: count }, (_, i) => 30 | createWorkItemFixture( 31 | i + 1, 32 | `Work Item ${i + 1}`, 33 | i % 2 === 0 ? 'Active' : 'Resolved', 34 | ), 35 | ); 36 | }; 37 | ``` -------------------------------------------------------------------------------- /docs/examples/azure-cli-authentication.env: -------------------------------------------------------------------------------- ``` 1 | # Example .env file for Azure CLI authentication 2 | # Replace the values with your own 3 | 4 | # Authentication method (required) 5 | AZURE_DEVOPS_AUTH_METHOD=azure-cli 6 | 7 | # Azure DevOps organization URL (required) 8 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization 9 | 10 | # Default project to use when not specified (optional) 11 | AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project 12 | 13 | # API Version to use (optional, defaults to latest) 14 | # AZURE_DEVOPS_API_VERSION=6.0 15 | 16 | # Logging Level (optional) 17 | LOG_LEVEL=info 18 | 19 | # Note: This server uses stdio for communication with the MCP client, 20 | # not HTTP. It does not listen on a network port. 21 | 22 | # Note: Before using Azure CLI authentication, make sure you have: 23 | # 1. Installed the Azure CLI (https://docs.microsoft.com/cli/azure/install-azure-cli) 24 | # 2. Logged in with 'az login' 25 | # 3. Verified your account has access to the Azure DevOps organization ``` -------------------------------------------------------------------------------- /src/features/repositories/list-repositories/feature.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { AzureDevOpsError } from '../../../shared/errors'; 3 | import { ListRepositoriesOptions, GitRepository } from '../types'; 4 | 5 | /** 6 | * List repositories in a project 7 | * 8 | * @param connection The Azure DevOps WebApi connection 9 | * @param options Parameters for listing repositories 10 | * @returns Array of repositories 11 | */ 12 | export async function listRepositories( 13 | connection: WebApi, 14 | options: ListRepositoriesOptions, 15 | ): Promise<GitRepository[]> { 16 | try { 17 | const gitApi = await connection.getGitApi(); 18 | const repositories = await gitApi.getRepositories( 19 | options.projectId, 20 | options.includeLinks, 21 | ); 22 | 23 | return repositories; 24 | } catch (error) { 25 | if (error instanceof AzureDevOpsError) { 26 | throw error; 27 | } 28 | throw new Error( 29 | `Failed to list repositories: ${error instanceof Error ? error.message : String(error)}`, 30 | ); 31 | } 32 | } 33 | ``` -------------------------------------------------------------------------------- /src/features/projects/list-projects/feature.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { AzureDevOpsError } from '../../../shared/errors'; 3 | import { ListProjectsOptions, TeamProject } from '../types'; 4 | 5 | /** 6 | * List all projects in the organization 7 | * 8 | * @param connection The Azure DevOps WebApi connection 9 | * @param options Optional parameters for listing projects 10 | * @returns Array of projects 11 | */ 12 | export async function listProjects( 13 | connection: WebApi, 14 | options: ListProjectsOptions = {}, 15 | ): Promise<TeamProject[]> { 16 | try { 17 | const coreApi = await connection.getCoreApi(); 18 | const projects = await coreApi.getProjects( 19 | options.stateFilter, 20 | options.top, 21 | options.skip, 22 | options.continuationToken, 23 | ); 24 | 25 | return projects; 26 | } catch (error) { 27 | if (error instanceof AzureDevOpsError) { 28 | throw error; 29 | } 30 | throw new Error( 31 | `Failed to list projects: ${error instanceof Error ? error.message : String(error)}`, 32 | ); 33 | } 34 | } 35 | ``` -------------------------------------------------------------------------------- /src/features/work-items/__test__/test-utils.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Test utilities for work item tests 3 | * These utilities help reduce test execution time and improve test reliability 4 | */ 5 | 6 | /** 7 | * Times test execution to help identify slow tests 8 | * @param testName Name of the test 9 | * @param fn Test function to execute 10 | */ 11 | export async function timeTest(testName: string, fn: () => Promise<void>) { 12 | const start = performance.now(); 13 | await fn(); 14 | const end = performance.now(); 15 | 16 | const duration = end - start; 17 | if (duration > 100) { 18 | console.warn(`Test "${testName}" is slow (${duration.toFixed(2)}ms)`); 19 | } 20 | return duration; 21 | } 22 | 23 | /** 24 | * Setup function to prepare test environment 25 | * Call at beginning of test to ensure consistent setup 26 | */ 27 | export function setupTestEnvironment() { 28 | // Set any environment variables needed for tests 29 | const originalEnv = { ...process.env }; 30 | 31 | return { 32 | // Clean up function to restore environment 33 | cleanup: () => { 34 | // Restore original environment 35 | process.env = originalEnv; 36 | }, 37 | }; 38 | } 39 | ``` -------------------------------------------------------------------------------- /src/utils/environment.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Load environment variables 2 | import dotenv from 'dotenv'; 3 | dotenv.config(); 4 | 5 | /** 6 | * Utility functions and constants related to environment variables. 7 | */ 8 | 9 | /** 10 | * Extract organization name from Azure DevOps organization URL 11 | */ 12 | export function getOrgNameFromUrl(url?: string): string { 13 | if (!url) return 'unknown-organization'; 14 | const devMatch = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)/); 15 | if (devMatch) { 16 | return devMatch[1]; 17 | } 18 | // Fallback only for Azure DevOps Server URLs 19 | if (url.includes('azure')) { 20 | const fallbackMatch = url.match(/https?:\/\/[^/]+\/([^/]+)/); 21 | return fallbackMatch ? fallbackMatch[1] : 'unknown-organization'; 22 | } 23 | return 'unknown-organization'; 24 | } 25 | 26 | /** 27 | * Default project name from environment variables 28 | */ 29 | export const defaultProject = 30 | process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'no default project'; 31 | 32 | /** 33 | * Default organization name derived from the organization URL 34 | */ 35 | export const defaultOrg = getOrgNameFromUrl(process.env.AZURE_DEVOPS_ORG_URL); 36 | ``` -------------------------------------------------------------------------------- /src/features/projects/get-project/feature.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { 3 | AzureDevOpsResourceNotFoundError, 4 | AzureDevOpsError, 5 | } from '../../../shared/errors'; 6 | import { TeamProject } from '../types'; 7 | 8 | /** 9 | * Get a project by ID or name 10 | * 11 | * @param connection The Azure DevOps WebApi connection 12 | * @param projectId The ID or name of the project 13 | * @returns The project details 14 | * @throws {AzureDevOpsResourceNotFoundError} If the project is not found 15 | */ 16 | export async function getProject( 17 | connection: WebApi, 18 | projectId: string, 19 | ): Promise<TeamProject> { 20 | try { 21 | const coreApi = await connection.getCoreApi(); 22 | const project = await coreApi.getProject(projectId); 23 | 24 | if (!project) { 25 | throw new AzureDevOpsResourceNotFoundError( 26 | `Project '${projectId}' not found`, 27 | ); 28 | } 29 | 30 | return project; 31 | } catch (error) { 32 | if (error instanceof AzureDevOpsError) { 33 | throw error; 34 | } 35 | throw new Error( 36 | `Failed to get project: ${error instanceof Error ? error.message : String(error)}`, 37 | ); 38 | } 39 | } 40 | ``` -------------------------------------------------------------------------------- /src/features/projects/__test__/test-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { getPersonalAccessTokenHandler } from 'azure-devops-node-api'; 3 | 4 | /** 5 | * Creates a WebApi connection for tests with real credentials 6 | * 7 | * @returns WebApi connection 8 | */ 9 | export async function getTestConnection(): Promise<WebApi | null> { 10 | // If we have real credentials, use them 11 | const orgUrl = process.env.AZURE_DEVOPS_ORG_URL; 12 | const token = process.env.AZURE_DEVOPS_PAT; 13 | 14 | if (orgUrl && token) { 15 | const authHandler = getPersonalAccessTokenHandler(token); 16 | return new WebApi(orgUrl, authHandler); 17 | } 18 | 19 | // If we don't have credentials, return null 20 | return null; 21 | } 22 | 23 | /** 24 | * Determines if integration tests should be skipped 25 | * 26 | * @returns true if integration tests should be skipped 27 | */ 28 | export function shouldSkipIntegrationTest(): boolean { 29 | if (!process.env.AZURE_DEVOPS_ORG_URL || !process.env.AZURE_DEVOPS_PAT) { 30 | console.log( 31 | 'Skipping integration test: No real Azure DevOps connection available', 32 | ); 33 | return true; 34 | } 35 | return false; 36 | } 37 | ``` -------------------------------------------------------------------------------- /src/features/repositories/__test__/test-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { getPersonalAccessTokenHandler } from 'azure-devops-node-api'; 3 | 4 | /** 5 | * Creates a WebApi connection for tests with real credentials 6 | * 7 | * @returns WebApi connection 8 | */ 9 | export async function getTestConnection(): Promise<WebApi | null> { 10 | // If we have real credentials, use them 11 | const orgUrl = process.env.AZURE_DEVOPS_ORG_URL; 12 | const token = process.env.AZURE_DEVOPS_PAT; 13 | 14 | if (orgUrl && token) { 15 | const authHandler = getPersonalAccessTokenHandler(token); 16 | return new WebApi(orgUrl, authHandler); 17 | } 18 | 19 | // If we don't have credentials, return null 20 | return null; 21 | } 22 | 23 | /** 24 | * Determines if integration tests should be skipped 25 | * 26 | * @returns true if integration tests should be skipped 27 | */ 28 | export function shouldSkipIntegrationTest(): boolean { 29 | if (!process.env.AZURE_DEVOPS_ORG_URL || !process.env.AZURE_DEVOPS_PAT) { 30 | console.log( 31 | 'Skipping integration test: No real Azure DevOps connection available', 32 | ); 33 | return true; 34 | } 35 | return false; 36 | } 37 | ``` -------------------------------------------------------------------------------- /src/features/work-items/__test__/test-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { getPersonalAccessTokenHandler } from 'azure-devops-node-api'; 3 | 4 | /** 5 | * Creates a WebApi connection for tests with real credentials 6 | * 7 | * @returns WebApi connection 8 | */ 9 | export async function getTestConnection(): Promise<WebApi | null> { 10 | // If we have real credentials, use them 11 | const orgUrl = process.env.AZURE_DEVOPS_ORG_URL; 12 | const token = process.env.AZURE_DEVOPS_PAT; 13 | 14 | if (orgUrl && token) { 15 | const authHandler = getPersonalAccessTokenHandler(token); 16 | return new WebApi(orgUrl, authHandler); 17 | } 18 | 19 | // If we don't have credentials, return null 20 | return null; 21 | } 22 | 23 | /** 24 | * Determines if integration tests should be skipped 25 | * 26 | * @returns true if integration tests should be skipped 27 | */ 28 | export function shouldSkipIntegrationTest(): boolean { 29 | if (!process.env.AZURE_DEVOPS_ORG_URL || !process.env.AZURE_DEVOPS_PAT) { 30 | console.log( 31 | 'Skipping integration test: No real Azure DevOps connection available', 32 | ); 33 | return true; 34 | } 35 | return false; 36 | } 37 | ``` -------------------------------------------------------------------------------- /src/features/wikis/create-wiki-page/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { defaultProject, defaultOrg } from '../../../utils/environment'; 3 | 4 | /** 5 | * Schema for creating a new wiki page in Azure DevOps 6 | */ 7 | export const CreateWikiPageSchema = z.object({ 8 | organizationId: z 9 | .string() 10 | .optional() 11 | .nullable() 12 | .describe(`The ID or name of the organization (Default: ${defaultOrg})`), 13 | projectId: z 14 | .string() 15 | .optional() 16 | .nullable() 17 | .describe(`The ID or name of the project (Default: ${defaultProject})`), 18 | wikiId: z.string().min(1).describe('The ID or name of the wiki'), 19 | pagePath: z 20 | .string() 21 | .optional() 22 | .nullable() 23 | .default('/') 24 | .describe( 25 | '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', 26 | ), 27 | content: z 28 | .string() 29 | .min(1) 30 | .describe('The content for the new wiki page in markdown format'), 31 | comment: z 32 | .string() 33 | .optional() 34 | .describe('Optional comment for the creation or update'), 35 | }); 36 | ``` -------------------------------------------------------------------------------- /src/features/work-items/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | WorkItem, 3 | WorkItemReference, 4 | } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces'; 5 | 6 | /** 7 | * Options for listing work items 8 | */ 9 | export interface ListWorkItemsOptions { 10 | projectId: string; 11 | teamId?: string; 12 | queryId?: string; 13 | wiql?: string; 14 | top?: number; 15 | skip?: number; 16 | } 17 | 18 | /** 19 | * Options for creating a work item 20 | */ 21 | export interface CreateWorkItemOptions { 22 | title: string; 23 | description?: string; 24 | assignedTo?: string; 25 | areaPath?: string; 26 | iterationPath?: string; 27 | priority?: number; 28 | parentId?: number; 29 | additionalFields?: Record<string, string | number | boolean | null>; 30 | } 31 | 32 | /** 33 | * Options for updating a work item 34 | */ 35 | export interface UpdateWorkItemOptions { 36 | title?: string; 37 | description?: string; 38 | assignedTo?: string; 39 | areaPath?: string; 40 | iterationPath?: string; 41 | priority?: number; 42 | state?: string; 43 | additionalFields?: Record<string, string | number | boolean | null>; 44 | } 45 | 46 | // Re-export WorkItem and WorkItemReference types for convenience 47 | export type { WorkItem, WorkItemReference }; 48 | ``` -------------------------------------------------------------------------------- /src/features/work-items/tool-definitions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { zodToJsonSchema } from 'zod-to-json-schema'; 2 | import { ToolDefinition } from '../../shared/types/tool-definition'; 3 | import { 4 | ListWorkItemsSchema, 5 | CreateWorkItemSchema, 6 | UpdateWorkItemSchema, 7 | ManageWorkItemLinkSchema, 8 | GetWorkItemSchema, 9 | } from './schemas'; 10 | 11 | /** 12 | * List of work items tools 13 | */ 14 | export const workItemsTools: ToolDefinition[] = [ 15 | { 16 | name: 'list_work_items', 17 | description: 'List work items in a project', 18 | inputSchema: zodToJsonSchema(ListWorkItemsSchema), 19 | }, 20 | { 21 | name: 'get_work_item', 22 | description: 'Get details of a specific work item', 23 | inputSchema: zodToJsonSchema(GetWorkItemSchema), 24 | }, 25 | { 26 | name: 'create_work_item', 27 | description: 'Create a new work item', 28 | inputSchema: zodToJsonSchema(CreateWorkItemSchema), 29 | }, 30 | { 31 | name: 'update_work_item', 32 | description: 'Update an existing work item', 33 | inputSchema: zodToJsonSchema(UpdateWorkItemSchema), 34 | }, 35 | { 36 | name: 'manage_work_item_link', 37 | description: 'Add or remove links between work items', 38 | inputSchema: zodToJsonSchema(ManageWorkItemLinkSchema), 39 | }, 40 | ]; 41 | ``` -------------------------------------------------------------------------------- /src/features/organizations/__test__/test-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { AzureDevOpsConfig } from '../../../shared/types'; 2 | import { AuthenticationMethod } from '../../../shared/auth'; 3 | 4 | /** 5 | * Creates test configuration for Azure DevOps tests 6 | * 7 | * @returns Azure DevOps config 8 | */ 9 | export function getTestConfig(): AzureDevOpsConfig | null { 10 | // If we have real credentials, use them 11 | const orgUrl = process.env.AZURE_DEVOPS_ORG_URL; 12 | const pat = process.env.AZURE_DEVOPS_PAT; 13 | 14 | if (orgUrl && pat) { 15 | return { 16 | organizationUrl: orgUrl, 17 | authMethod: AuthenticationMethod.PersonalAccessToken, 18 | personalAccessToken: pat, 19 | defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT, 20 | }; 21 | } 22 | 23 | // If we don't have credentials, return null 24 | return null; 25 | } 26 | 27 | /** 28 | * Determines if integration tests should be skipped 29 | * 30 | * @returns true if integration tests should be skipped 31 | */ 32 | export function shouldSkipIntegrationTest(): boolean { 33 | if (!process.env.AZURE_DEVOPS_ORG_URL || !process.env.AZURE_DEVOPS_PAT) { 34 | console.log( 35 | 'Skipping integration test: No real Azure DevOps connection available', 36 | ); 37 | return true; 38 | } 39 | return false; 40 | } 41 | ``` -------------------------------------------------------------------------------- /src/features/wikis/update-wiki-page/feature.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as azureDevOpsClient from '../../../clients/azure-devops'; 2 | import { UpdateWikiPageSchema } from './schema'; 3 | import { z } from 'zod'; 4 | import { defaultOrg, defaultProject } from '../../../utils/environment'; 5 | 6 | /** 7 | * Options for updating a wiki page 8 | */ 9 | export type UpdateWikiPageOptions = z.infer<typeof UpdateWikiPageSchema>; 10 | 11 | /** 12 | * Updates a wiki page in Azure DevOps 13 | * @param options - The options for updating the wiki page 14 | * @returns The updated wiki page 15 | */ 16 | export async function updateWikiPage(options: UpdateWikiPageOptions) { 17 | const validatedOptions = UpdateWikiPageSchema.parse(options); 18 | 19 | const { organizationId, projectId, wikiId, pagePath, content, comment } = 20 | validatedOptions; 21 | 22 | // Create the client 23 | const client = await azureDevOpsClient.getWikiClient({ 24 | organizationId: organizationId ?? defaultOrg, 25 | }); 26 | 27 | // Prepare the wiki page content 28 | const wikiPageContent = { 29 | content, 30 | }; 31 | 32 | // Update the wiki page 33 | const updatedPage = await client.updatePage( 34 | wikiPageContent, 35 | projectId ?? defaultProject, 36 | wikiId, 37 | pagePath, 38 | { 39 | comment: comment ?? undefined, 40 | }, 41 | ); 42 | 43 | return updatedPage; 44 | } 45 | ``` -------------------------------------------------------------------------------- /docs/examples/azure-identity-authentication.env: -------------------------------------------------------------------------------- ``` 1 | # Example .env file for Azure Identity (DefaultAzureCredential) authentication 2 | # Replace the values with your own 3 | 4 | # Authentication method (required) 5 | AZURE_DEVOPS_AUTH_METHOD=azure-identity 6 | 7 | # Azure DevOps organization URL (required) 8 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization 9 | 10 | # Default project to use when not specified (optional) 11 | AZURE_DEVOPS_DEFAULT_PROJECT=your-default-project 12 | 13 | # API Version to use (optional, defaults to latest) 14 | # AZURE_DEVOPS_API_VERSION=6.0 15 | 16 | # Azure AD tenant ID (required for service principal authentication) 17 | # AZURE_TENANT_ID=your-tenant-id 18 | 19 | # Azure AD client ID (required for service principal authentication) 20 | # AZURE_CLIENT_ID=your-client-id 21 | 22 | # Azure AD client secret (required for service principal authentication) 23 | # AZURE_CLIENT_SECRET=your-client-secret 24 | 25 | # Logging Level (optional) 26 | LOG_LEVEL=info 27 | 28 | # Note: This server uses stdio for communication with the MCP client, 29 | # not HTTP. It does not listen on a network port. 30 | 31 | # Note: When using DefaultAzureCredential, you don't need to set AZURE_TENANT_ID, 32 | # AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET if you're using other credential types 33 | # like Managed Identity or Azure CLI. ``` -------------------------------------------------------------------------------- /src/features/repositories/get-repository/feature.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { 3 | AzureDevOpsResourceNotFoundError, 4 | AzureDevOpsError, 5 | } from '../../../shared/errors'; 6 | import { GitRepository } from '../types'; 7 | 8 | /** 9 | * Get a repository by ID or name 10 | * 11 | * @param connection The Azure DevOps WebApi connection 12 | * @param projectId The ID or name of the project 13 | * @param repositoryId The ID or name of the repository 14 | * @returns The repository details 15 | * @throws {AzureDevOpsResourceNotFoundError} If the repository is not found 16 | */ 17 | export async function getRepository( 18 | connection: WebApi, 19 | projectId: string, 20 | repositoryId: string, 21 | ): Promise<GitRepository> { 22 | try { 23 | const gitApi = await connection.getGitApi(); 24 | const repository = await gitApi.getRepository(repositoryId, projectId); 25 | 26 | if (!repository) { 27 | throw new AzureDevOpsResourceNotFoundError( 28 | `Repository '${repositoryId}' not found in project '${projectId}'`, 29 | ); 30 | } 31 | 32 | return repository; 33 | } catch (error) { 34 | if (error instanceof AzureDevOpsError) { 35 | throw error; 36 | } 37 | throw new Error( 38 | `Failed to get repository: ${error instanceof Error ? error.message : String(error)}`, 39 | ); 40 | } 41 | } 42 | ``` -------------------------------------------------------------------------------- /src/features/users/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Users feature module 3 | * 4 | * This module contains user-related functionality. 5 | */ 6 | 7 | export * from './types'; 8 | export * from './get-me'; 9 | 10 | // Export tool definitions 11 | export * from './tool-definitions'; 12 | 13 | // New exports for request handling 14 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; 15 | import { WebApi } from 'azure-devops-node-api'; 16 | import { 17 | RequestIdentifier, 18 | RequestHandler, 19 | } from '../../shared/types/request-handler'; 20 | import { getMe } from './'; 21 | 22 | /** 23 | * Checks if the request is for the users feature 24 | */ 25 | export const isUsersRequest: RequestIdentifier = ( 26 | request: CallToolRequest, 27 | ): boolean => { 28 | const toolName = request.params.name; 29 | return ['get_me'].includes(toolName); 30 | }; 31 | 32 | /** 33 | * Handles users feature requests 34 | */ 35 | export const handleUsersRequest: RequestHandler = async ( 36 | connection: WebApi, 37 | request: CallToolRequest, 38 | ): Promise<{ content: Array<{ type: string; text: string }> }> => { 39 | switch (request.params.name) { 40 | case 'get_me': { 41 | const result = await getMe(connection); 42 | return { 43 | content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], 44 | }; 45 | } 46 | default: 47 | throw new Error(`Unknown users tool: ${request.params.name}`); 48 | } 49 | }; 50 | ``` -------------------------------------------------------------------------------- /src/features/wikis/get-wikis/feature.spec.int.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { getWikis } from './feature'; 3 | import { 4 | getTestConnection, 5 | shouldSkipIntegrationTest, 6 | } from '@/shared/test/test-helpers'; 7 | 8 | describe('getWikis integration', () => { 9 | let connection: WebApi | null = null; 10 | let projectName: string; 11 | 12 | beforeAll(async () => { 13 | // Get a real connection using environment variables 14 | connection = await getTestConnection(); 15 | projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject'; 16 | }); 17 | 18 | test('should retrieve wikis from Azure DevOps', async () => { 19 | // Skip if no connection is available 20 | if (shouldSkipIntegrationTest()) { 21 | return; 22 | } 23 | 24 | // This connection must be available if we didn't skip 25 | if (!connection) { 26 | throw new Error( 27 | 'Connection should be available when test is not skipped', 28 | ); 29 | } 30 | 31 | // Get the wikis 32 | const result = await getWikis(connection, { 33 | projectId: projectName, 34 | }); 35 | 36 | // Verify the result 37 | expect(result).toBeDefined(); 38 | expect(Array.isArray(result)).toBe(true); 39 | if (result.length > 0) { 40 | expect(result[0].name).toBeDefined(); 41 | expect(result[0].id).toBeDefined(); 42 | expect(result[0].type).toBeDefined(); 43 | } 44 | }); 45 | }); 46 | ``` -------------------------------------------------------------------------------- /src/features/users/get-me/feature.spec.int.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { getMe } from '../get-me'; 3 | import { 4 | getTestConnection, 5 | shouldSkipIntegrationTest, 6 | } from '@/shared/test/test-helpers'; 7 | 8 | describe('getMe Integration', () => { 9 | let connection: WebApi | null = null; 10 | 11 | beforeAll(async () => { 12 | // Get a real connection using environment variables 13 | connection = await getTestConnection(); 14 | }); 15 | 16 | test('should get authenticated user profile information', async () => { 17 | // Skip if no connection is available 18 | if (shouldSkipIntegrationTest() || !connection) { 19 | console.log('Skipping getMe integration test - no connection available'); 20 | return; 21 | } 22 | 23 | // Act - make a direct API call using Axios 24 | const result = await getMe(connection); 25 | 26 | // Assert on the actual response 27 | expect(result).toBeDefined(); 28 | expect(result.id).toBeDefined(); 29 | expect(typeof result.id).toBe('string'); 30 | expect(result.displayName).toBeDefined(); 31 | expect(typeof result.displayName).toBe('string'); 32 | expect(result.displayName.length).toBeGreaterThan(0); 33 | 34 | // Email should be defined, a string, and not empty 35 | expect(result.email).toBeDefined(); 36 | expect(typeof result.email).toBe('string'); 37 | expect(result.email.length).toBeGreaterThan(0); 38 | }); 39 | }); 40 | ``` -------------------------------------------------------------------------------- /src/features/work-items/get-work-item/feature.spec.unit.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getWorkItem } from './feature'; 2 | import { AzureDevOpsError } from '../../../shared/errors'; 3 | 4 | // Unit tests should only focus on isolated logic 5 | // No real connections, HTTP requests, or dependencies 6 | describe('getWorkItem unit', () => { 7 | // Unit test for error handling logic - the only part that's suitable for a unit test 8 | test('should propagate custom errors when thrown internally', async () => { 9 | // Arrange - for unit test, we mock only what's needed 10 | const mockConnection: any = { 11 | getWorkItemTrackingApi: jest.fn().mockImplementation(() => { 12 | throw new AzureDevOpsError('Custom error'); 13 | }), 14 | }; 15 | 16 | // Act & Assert 17 | await expect(getWorkItem(mockConnection, 123)).rejects.toThrow( 18 | AzureDevOpsError, 19 | ); 20 | await expect(getWorkItem(mockConnection, 123)).rejects.toThrow( 21 | 'Custom error', 22 | ); 23 | }); 24 | 25 | test('should wrap unexpected errors in a friendly error message', async () => { 26 | // Arrange 27 | const mockConnection: any = { 28 | getWorkItemTrackingApi: jest.fn().mockImplementation(() => { 29 | throw new Error('Unexpected error'); 30 | }), 31 | }; 32 | 33 | // Act & Assert 34 | await expect(getWorkItem(mockConnection, 123)).rejects.toThrow( 35 | 'Failed to get work item: Unexpected error', 36 | ); 37 | }); 38 | }); 39 | ``` -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Use latest Node LTS 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 'lts/*' 20 | - name: Install Dependencies 21 | run: npm install 22 | - name: Lint 23 | run: npm run lint 24 | - name: Build 25 | run: npm run build 26 | - name: Unit Tests 27 | run: npm run test:unit 28 | - name: Integration Tests 29 | run: npm run test:int 30 | env: 31 | CI: 'true' 32 | AZURE_DEVOPS_ORG_URL: ${{ secrets.AZURE_DEVOPS_ORG_URL }} 33 | AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} 34 | AZURE_DEVOPS_DEFAULT_PROJECT: ${{ secrets.AZURE_DEVOPS_DEFAULT_PROJECT }} 35 | AZURE_DEVOPS_DEFAULT_REPOSITORY: eShopOnWeb 36 | AZURE_DEVOPS_AUTH_METHOD: pat 37 | - name: E2E Tests 38 | run: npm run test:e2e 39 | env: 40 | CI: 'true' 41 | AZURE_DEVOPS_ORG_URL: ${{ secrets.AZURE_DEVOPS_ORG_URL }} 42 | AZURE_DEVOPS_PAT: ${{ secrets.AZURE_DEVOPS_PAT }} 43 | AZURE_DEVOPS_DEFAULT_PROJECT: ${{ secrets.AZURE_DEVOPS_DEFAULT_PROJECT }} 44 | AZURE_DEVOPS_DEFAULT_REPOSITORY: eShopOnWeb 45 | AZURE_DEVOPS_AUTH_METHOD: pat 46 | ``` -------------------------------------------------------------------------------- /src/features/pipelines/trigger-pipeline/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { defaultProject } from '../../../utils/environment'; 3 | 4 | /** 5 | * Schema for the triggerPipeline function 6 | */ 7 | export const TriggerPipelineSchema = z.object({ 8 | // The project containing the pipeline 9 | projectId: z 10 | .string() 11 | .optional() 12 | .describe(`The ID or name of the project (Default: ${defaultProject})`), 13 | // The ID of the pipeline to trigger 14 | pipelineId: z 15 | .number() 16 | .int() 17 | .positive() 18 | .describe('The numeric ID of the pipeline to trigger'), 19 | // The branch to run the pipeline on 20 | branch: z 21 | .string() 22 | .optional() 23 | .describe( 24 | 'The branch to run the pipeline on (e.g., "main", "feature/my-branch"). If left empty, the default branch will be used', 25 | ), 26 | // Variables to pass to the pipeline run 27 | variables: z 28 | .record( 29 | z.object({ 30 | value: z.string(), 31 | isSecret: z.boolean().optional(), 32 | }), 33 | ) 34 | .optional() 35 | .describe('Variables to pass to the pipeline run'), 36 | // Parameters for template-based pipelines 37 | templateParameters: z 38 | .record(z.string()) 39 | .optional() 40 | .describe('Parameters for template-based pipelines'), 41 | // Stages to skip in the pipeline run 42 | stagesToSkip: z 43 | .array(z.string()) 44 | .optional() 45 | .describe('Stages to skip in the pipeline run'), 46 | }); 47 | ``` -------------------------------------------------------------------------------- /src/features/pull-requests/tool-definitions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { zodToJsonSchema } from 'zod-to-json-schema'; 2 | import { ToolDefinition } from '../../shared/types/tool-definition'; 3 | import { 4 | CreatePullRequestSchema, 5 | ListPullRequestsSchema, 6 | GetPullRequestCommentsSchema, 7 | AddPullRequestCommentSchema, 8 | UpdatePullRequestSchema, 9 | } from './schemas'; 10 | 11 | /** 12 | * List of pull requests tools 13 | */ 14 | export const pullRequestsTools: ToolDefinition[] = [ 15 | { 16 | name: 'create_pull_request', 17 | description: 'Create a new pull request', 18 | inputSchema: zodToJsonSchema(CreatePullRequestSchema), 19 | }, 20 | { 21 | name: 'list_pull_requests', 22 | description: 'List pull requests in a repository', 23 | inputSchema: zodToJsonSchema(ListPullRequestsSchema), 24 | }, 25 | { 26 | name: 'get_pull_request_comments', 27 | description: 'Get comments from a specific pull request', 28 | inputSchema: zodToJsonSchema(GetPullRequestCommentsSchema), 29 | }, 30 | { 31 | name: 'add_pull_request_comment', 32 | description: 33 | 'Add a comment to a pull request (reply to existing comments or create new threads)', 34 | inputSchema: zodToJsonSchema(AddPullRequestCommentSchema), 35 | }, 36 | { 37 | name: 'update_pull_request', 38 | description: 39 | 'Update an existing pull request with new properties, link work items, and manage reviewers', 40 | inputSchema: zodToJsonSchema(UpdatePullRequestSchema), 41 | }, 42 | ]; 43 | ``` -------------------------------------------------------------------------------- /src/features/repositories/tool-definitions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { zodToJsonSchema } from 'zod-to-json-schema'; 2 | import { ToolDefinition } from '../../shared/types/tool-definition'; 3 | import { 4 | GetRepositorySchema, 5 | GetRepositoryDetailsSchema, 6 | ListRepositoriesSchema, 7 | GetFileContentSchema, 8 | GetAllRepositoriesTreeSchema, 9 | } from './schemas'; 10 | 11 | /** 12 | * List of repositories tools 13 | */ 14 | export const repositoriesTools: ToolDefinition[] = [ 15 | { 16 | name: 'get_repository', 17 | description: 'Get details of a specific repository', 18 | inputSchema: zodToJsonSchema(GetRepositorySchema), 19 | }, 20 | { 21 | name: 'get_repository_details', 22 | description: 23 | 'Get detailed information about a repository including statistics and refs', 24 | inputSchema: zodToJsonSchema(GetRepositoryDetailsSchema), 25 | }, 26 | { 27 | name: 'list_repositories', 28 | description: 'List repositories in a project', 29 | inputSchema: zodToJsonSchema(ListRepositoriesSchema), 30 | }, 31 | { 32 | name: 'get_file_content', 33 | description: 'Get content of a file or directory from a repository', 34 | inputSchema: zodToJsonSchema(GetFileContentSchema), 35 | }, 36 | { 37 | name: 'get_all_repositories_tree', 38 | description: 39 | 'Displays a hierarchical tree view of files and directories across multiple Azure DevOps repositories within a project, based on their default branches', 40 | inputSchema: zodToJsonSchema(GetAllRepositoriesTreeSchema), 41 | }, 42 | ]; 43 | ``` -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Release Please 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | issues: write 12 | 13 | jobs: 14 | release-please: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: google-github-actions/release-please-action@v4 18 | id: release 19 | with: 20 | config-file: .github/release-please-config.json 21 | manifest-file: .github/release-please-manifest.json 22 | 23 | # The following steps only run if a new release is created 24 | - name: Checkout code 25 | if: ${{ steps.release.outputs.release_created }} 26 | uses: actions/checkout@v3 27 | with: 28 | ref: ${{ steps.release.outputs.tag_name }} 29 | 30 | - name: Setup Node.js 31 | if: ${{ steps.release.outputs.release_created }} 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: 'lts/*' 35 | registry-url: 'https://registry.npmjs.org/' 36 | 37 | - name: Install Dependencies 38 | if: ${{ steps.release.outputs.release_created }} 39 | run: npm ci 40 | 41 | - name: Build package 42 | if: ${{ steps.release.outputs.release_created }} 43 | run: npm run build 44 | 45 | - name: Publish to npm 46 | if: ${{ steps.release.outputs.release_created }} 47 | run: npm publish --access public 48 | env: 49 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 50 | ``` -------------------------------------------------------------------------------- /src/features/wikis/tool-definitions.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { zodToJsonSchema } from 'zod-to-json-schema'; 2 | import { ToolDefinition } from '../../shared/types/tool-definition'; 3 | import { GetWikisSchema } from './get-wikis/schema'; 4 | import { GetWikiPageSchema } from './get-wiki-page/schema'; 5 | import { CreateWikiSchema } from './create-wiki/schema'; 6 | import { UpdateWikiPageSchema } from './update-wiki-page/schema'; 7 | import { ListWikiPagesSchema } from './list-wiki-pages/schema'; 8 | import { CreateWikiPageSchema } from './create-wiki-page/schema'; 9 | 10 | /** 11 | * List of wikis tools 12 | */ 13 | export const wikisTools: ToolDefinition[] = [ 14 | { 15 | name: 'get_wikis', 16 | description: 'Get details of wikis in a project', 17 | inputSchema: zodToJsonSchema(GetWikisSchema), 18 | }, 19 | { 20 | name: 'get_wiki_page', 21 | description: 'Get the content of a wiki page', 22 | inputSchema: zodToJsonSchema(GetWikiPageSchema), 23 | }, 24 | { 25 | name: 'create_wiki', 26 | description: 'Create a new wiki in the project', 27 | inputSchema: zodToJsonSchema(CreateWikiSchema), 28 | }, 29 | { 30 | name: 'update_wiki_page', 31 | description: 'Update content of a wiki page', 32 | inputSchema: zodToJsonSchema(UpdateWikiPageSchema), 33 | }, 34 | { 35 | name: 'list_wiki_pages', 36 | description: 'List pages within an Azure DevOps wiki', 37 | inputSchema: zodToJsonSchema(ListWikiPagesSchema), 38 | }, 39 | { 40 | name: 'create_wiki_page', 41 | description: 42 | 'Create a new page in a wiki. If the page already exists at the specified path, it will be updated.', 43 | inputSchema: zodToJsonSchema(CreateWikiPageSchema), 44 | }, 45 | ]; 46 | ``` -------------------------------------------------------------------------------- /src/features/organizations/list-organizations/feature.spec.int.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { listOrganizations } from './feature'; 2 | import { 3 | getTestConfig, 4 | shouldSkipIntegrationTest, 5 | } from '@/shared/test/test-helpers'; 6 | 7 | describe('listOrganizations integration', () => { 8 | test('should list organizations accessible to the authenticated user', async () => { 9 | // Skip if no credentials are available 10 | if (shouldSkipIntegrationTest()) { 11 | return; 12 | } 13 | 14 | // Get test configuration 15 | const config = getTestConfig(); 16 | if (!config) { 17 | throw new Error( 18 | 'Configuration should be available when test is not skipped', 19 | ); 20 | } 21 | 22 | // Act - make an actual API call to Azure DevOps 23 | const result = await listOrganizations(config); 24 | 25 | // Assert on the actual response 26 | expect(result).toBeDefined(); 27 | expect(Array.isArray(result)).toBe(true); 28 | expect(result.length).toBeGreaterThan(0); 29 | 30 | // Check structure of returned organizations 31 | const firstOrg = result[0]; 32 | expect(firstOrg.id).toBeDefined(); 33 | expect(firstOrg.name).toBeDefined(); 34 | expect(firstOrg.url).toBeDefined(); 35 | 36 | // The organization URL in the config should match one of the returned organizations 37 | // Extract the organization name from the URL 38 | const orgUrlParts = config.organizationUrl.split('/'); 39 | const configOrgName = orgUrlParts[orgUrlParts.length - 1]; 40 | 41 | // Find matching organization 42 | const matchingOrg = result.find( 43 | (org) => org.name.toLowerCase() === configOrgName.toLowerCase(), 44 | ); 45 | expect(matchingOrg).toBeDefined(); 46 | }); 47 | }); 48 | ``` -------------------------------------------------------------------------------- /src/features/repositories/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | GitRepository, 3 | GitBranchStats, 4 | GitRef, 5 | GitItem, 6 | } from 'azure-devops-node-api/interfaces/GitInterfaces'; 7 | 8 | /** 9 | * Options for listing repositories 10 | */ 11 | export interface ListRepositoriesOptions { 12 | projectId: string; 13 | includeLinks?: boolean; 14 | } 15 | 16 | /** 17 | * Options for getting repository details 18 | */ 19 | export interface GetRepositoryDetailsOptions { 20 | projectId: string; 21 | repositoryId: string; 22 | includeStatistics?: boolean; 23 | includeRefs?: boolean; 24 | refFilter?: string; 25 | branchName?: string; 26 | } 27 | 28 | /** 29 | * Repository details response 30 | */ 31 | export interface RepositoryDetails { 32 | repository: GitRepository; 33 | statistics?: { 34 | branches: GitBranchStats[]; 35 | }; 36 | refs?: { 37 | value: GitRef[]; 38 | count: number; 39 | }; 40 | } 41 | 42 | /** 43 | * Options for getting all repositories tree 44 | */ 45 | export interface GetAllRepositoriesTreeOptions { 46 | organizationId: string; 47 | projectId: string; 48 | repositoryPattern?: string; 49 | depth?: number; 50 | pattern?: string; 51 | } 52 | 53 | /** 54 | * Repository tree item representation for output 55 | */ 56 | export interface RepositoryTreeItem { 57 | name: string; 58 | path: string; 59 | isFolder: boolean; 60 | level: number; 61 | } 62 | 63 | /** 64 | * Repository tree response for a single repository 65 | */ 66 | export interface RepositoryTreeResponse { 67 | name: string; 68 | tree: RepositoryTreeItem[]; 69 | stats: { 70 | directories: number; 71 | files: number; 72 | }; 73 | error?: string; 74 | } 75 | 76 | /** 77 | * Complete all repositories tree response 78 | */ 79 | export interface AllRepositoriesTreeResponse { 80 | repositories: RepositoryTreeResponse[]; 81 | } 82 | 83 | // Re-export GitRepository type for convenience 84 | export type { GitRepository, GitBranchStats, GitRef, GitItem }; 85 | ``` -------------------------------------------------------------------------------- /src/features/pipelines/list-pipelines/feature.spec.int.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { listPipelines } from './feature'; 3 | import { 4 | getTestConnection, 5 | shouldSkipIntegrationTest, 6 | } from '../../../shared/test/test-helpers'; 7 | 8 | describe('listPipelines integration', () => { 9 | let connection: WebApi | null = null; 10 | 11 | beforeAll(async () => { 12 | // Get a real connection using environment variables 13 | connection = await getTestConnection(); 14 | 15 | // TODO: Implement createPipeline functionality and create test pipelines here 16 | // Currently there is no way to create pipelines, so we can't ensure data exists like in list-work-items tests 17 | // In the future, we should add code similar to list-work-items to create test pipelines 18 | }); 19 | 20 | it('should list pipelines in a project', async () => { 21 | // Skip if no connection is available or no project specified 22 | if ( 23 | shouldSkipIntegrationTest() || 24 | !connection || 25 | !process.env.AZURE_DEVOPS_DEFAULT_PROJECT 26 | ) { 27 | console.log( 28 | 'Skipping listPipelines integration test - no connection or project available', 29 | ); 30 | return; 31 | } 32 | 33 | const projectId = process.env.AZURE_DEVOPS_DEFAULT_PROJECT; 34 | 35 | const pipelines = await listPipelines(connection, { projectId }); 36 | expect(Array.isArray(pipelines)).toBe(true); 37 | 38 | // If there are pipelines, check their structure 39 | if (pipelines.length > 0) { 40 | const pipeline = pipelines[0]; 41 | expect(pipeline.id).toBeDefined(); 42 | expect(pipeline.name).toBeDefined(); 43 | expect(pipeline.folder).toBeDefined(); 44 | expect(pipeline.revision).toBeDefined(); 45 | expect(pipeline.url).toBeDefined(); 46 | } 47 | }); 48 | }); 49 | ``` -------------------------------------------------------------------------------- /src/features/wikis/create-wiki/schema.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | 3 | import { defaultProject, defaultOrg } from '../../../utils/environment'; 4 | 5 | /** 6 | * Wiki types for creating wiki 7 | */ 8 | export enum WikiType { 9 | /** 10 | * The wiki is published from a git repository 11 | */ 12 | CodeWiki = 'codeWiki', 13 | 14 | /** 15 | * The wiki is provisioned for the team project 16 | */ 17 | ProjectWiki = 'projectWiki', 18 | } 19 | 20 | /** 21 | * Schema for creating a wiki in an Azure DevOps project 22 | */ 23 | export const CreateWikiSchema = z 24 | .object({ 25 | organizationId: z 26 | .string() 27 | .optional() 28 | .nullable() 29 | .describe(`The ID or name of the organization (Default: ${defaultOrg})`), 30 | projectId: z 31 | .string() 32 | .optional() 33 | .nullable() 34 | .describe(`The ID or name of the project (Default: ${defaultProject})`), 35 | name: z.string().describe('The name of the new wiki'), 36 | type: z 37 | .nativeEnum(WikiType) 38 | .optional() 39 | .default(WikiType.ProjectWiki) 40 | .describe('Type of wiki to create (projectWiki or codeWiki)'), 41 | repositoryId: z 42 | .string() 43 | .optional() 44 | .nullable() 45 | .describe( 46 | 'The ID of the repository to associate with the wiki (required for codeWiki)', 47 | ), 48 | mappedPath: z 49 | .string() 50 | .optional() 51 | .nullable() 52 | .default('/') 53 | .describe( 54 | 'Folder path inside repository which is shown as Wiki (only for codeWiki)', 55 | ), 56 | }) 57 | .refine( 58 | (data) => { 59 | // If type is codeWiki, then repositoryId is required 60 | return data.type !== WikiType.CodeWiki || !!data.repositoryId; 61 | }, 62 | { 63 | message: 'repositoryId is required when type is codeWiki', 64 | path: ['repositoryId'], 65 | }, 66 | ); 67 | ``` -------------------------------------------------------------------------------- /src/features/wikis/get-wiki-page/feature.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as azureDevOpsClient from '../../../clients/azure-devops'; 2 | import { AzureDevOpsError } from '../../../shared/errors/azure-devops-errors'; 3 | 4 | /** 5 | * Options for getting a wiki page 6 | */ 7 | export interface GetWikiPageOptions { 8 | /** 9 | * The ID or name of the organization 10 | * If not provided, the default organization will be used 11 | */ 12 | organizationId: string; 13 | 14 | /** 15 | * The ID or name of the project 16 | * If not provided, the default project will be used 17 | */ 18 | projectId: string; 19 | 20 | /** 21 | * The ID or name of the wiki 22 | */ 23 | wikiId: string; 24 | 25 | /** 26 | * The path of the page within the wiki 27 | */ 28 | pagePath: string; 29 | } 30 | 31 | /** 32 | * Get a wiki page from a wiki 33 | * 34 | * @param options Options for getting a wiki page 35 | * @returns Wiki page content as text/plain 36 | * @throws {AzureDevOpsResourceNotFoundError} When the wiki page is not found 37 | * @throws {AzureDevOpsPermissionError} When the user does not have permission to access the wiki page 38 | * @throws {AzureDevOpsError} When an error occurs while fetching the wiki page 39 | */ 40 | export async function getWikiPage( 41 | options: GetWikiPageOptions, 42 | ): Promise<string> { 43 | const { organizationId, projectId, wikiId, pagePath } = options; 44 | 45 | try { 46 | // Create the client 47 | const client = await azureDevOpsClient.getWikiClient({ 48 | organizationId, 49 | }); 50 | 51 | // Get the wiki page 52 | return (await client.getPage(projectId, wikiId, pagePath)).content; 53 | } catch (error) { 54 | // If it's already an AzureDevOpsError, rethrow it 55 | if (error instanceof AzureDevOpsError) { 56 | throw error; 57 | } 58 | // Otherwise wrap it in an AzureDevOpsError 59 | throw new AzureDevOpsError('Failed to get wiki page', { cause: error }); 60 | } 61 | } 62 | ``` -------------------------------------------------------------------------------- /src/shared/test/test-helpers.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { getPersonalAccessTokenHandler } from 'azure-devops-node-api'; 3 | import { AzureDevOpsConfig } from '../types'; 4 | import { AuthenticationMethod } from '../auth'; 5 | 6 | /** 7 | * Creates a WebApi connection for tests with real credentials 8 | * 9 | * @returns WebApi connection 10 | */ 11 | export async function getTestConnection(): Promise<WebApi | null> { 12 | // If we have real credentials, use them 13 | const orgUrl = process.env.AZURE_DEVOPS_ORG_URL; 14 | const token = process.env.AZURE_DEVOPS_PAT; 15 | 16 | if (orgUrl && token) { 17 | const authHandler = getPersonalAccessTokenHandler(token); 18 | return new WebApi(orgUrl, authHandler); 19 | } 20 | 21 | // If we don't have credentials, return null 22 | return null; 23 | } 24 | 25 | /** 26 | * Creates test configuration for Azure DevOps tests 27 | * 28 | * @returns Azure DevOps config 29 | */ 30 | export function getTestConfig(): AzureDevOpsConfig | null { 31 | // If we have real credentials, use them 32 | const orgUrl = process.env.AZURE_DEVOPS_ORG_URL; 33 | const pat = process.env.AZURE_DEVOPS_PAT; 34 | 35 | if (orgUrl && pat) { 36 | return { 37 | organizationUrl: orgUrl, 38 | authMethod: AuthenticationMethod.PersonalAccessToken, 39 | personalAccessToken: pat, 40 | defaultProject: process.env.AZURE_DEVOPS_DEFAULT_PROJECT, 41 | }; 42 | } 43 | 44 | // If we don't have credentials, return null 45 | return null; 46 | } 47 | 48 | /** 49 | * Determines if integration tests should be skipped 50 | * 51 | * @returns true if integration tests should be skipped 52 | */ 53 | export function shouldSkipIntegrationTest(): boolean { 54 | if (!process.env.AZURE_DEVOPS_ORG_URL || !process.env.AZURE_DEVOPS_PAT) { 55 | console.log( 56 | 'Skipping integration test: No real Azure DevOps connection available', 57 | ); 58 | return true; 59 | } 60 | return false; 61 | } 62 | ``` -------------------------------------------------------------------------------- /docs/testing/setup.md: -------------------------------------------------------------------------------- ```markdown 1 | # Testing Setup Guide 2 | 3 | ## Environment Variables 4 | 5 | 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. 6 | 7 | Required variables: 8 | ``` 9 | AZURE_DEVOPS_ORG_URL=https://dev.azure.com/your-organization 10 | AZURE_DEVOPS_PAT=your-personal-access-token 11 | AZURE_DEVOPS_DEFAULT_PROJECT=your-project-name 12 | ``` 13 | 14 | ## Test Structure 15 | 16 | Tests in this project are co-located with the code they're testing: 17 | 18 | ``` 19 | src/ 20 | features/ 21 | feature-name/ 22 | feature.ts 23 | feature.spec.unit.ts # Unit tests 24 | feature.spec.int.ts # Integration tests 25 | ``` 26 | 27 | E2E tests are only located at the server level: 28 | 29 | ``` 30 | src/ 31 | server.ts 32 | server.spec.e2e.ts # E2E tests 33 | ``` 34 | 35 | ## Import Pattern 36 | 37 | We use path aliases to make imports cleaner and easier to maintain. Instead of relative imports like: 38 | 39 | ```typescript 40 | import { someFunction } from '../../../../shared/utils'; 41 | ``` 42 | 43 | You can use the `@/` path alias: 44 | 45 | ```typescript 46 | import { someFunction } from '@/shared/utils'; 47 | ``` 48 | 49 | ### Test Helpers 50 | 51 | Test helpers are located in a centralized location for all tests: 52 | 53 | ```typescript 54 | import { getTestConnection, shouldSkipIntegrationTest } from '@/shared/test/test-helpers'; 55 | ``` 56 | 57 | ## Running Tests 58 | 59 | - Unit tests: `npm run test:unit` 60 | - Integration tests: `npm run test:int` 61 | - E2E tests: `npm run test:e2e` 62 | - All tests: `npm test` 63 | 64 | ## VSCode Integration 65 | 66 | The project includes VSCode settings that: 67 | 68 | 1. Show proper test icons for `*.spec.*.ts` files 69 | 2. Enable file nesting to group test files with their implementation 70 | 3. Configure TypeScript to prefer path aliases over relative imports 71 | 72 | These settings are stored in `.vscode/settings.json`. ``` -------------------------------------------------------------------------------- /src/features/work-items/update-work-item/feature.spec.unit.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { updateWorkItem } from './feature'; 2 | import { AzureDevOpsError } from '../../../shared/errors'; 3 | 4 | // Unit tests should only focus on isolated logic 5 | // No real connections, HTTP requests, or dependencies 6 | describe('updateWorkItem unit', () => { 7 | test('should throw error when no fields are provided for update', async () => { 8 | // Arrange - mock connection, never used due to validation error 9 | const mockConnection: any = { 10 | getWorkItemTrackingApi: jest.fn(), 11 | }; 12 | 13 | // Act & Assert - empty options object should throw 14 | await expect( 15 | updateWorkItem( 16 | mockConnection, 17 | 123, 18 | {}, // No fields to update 19 | ), 20 | ).rejects.toThrow('At least one field must be provided for update'); 21 | }); 22 | 23 | test('should propagate custom errors when thrown internally', async () => { 24 | // Arrange 25 | const mockConnection: any = { 26 | getWorkItemTrackingApi: jest.fn().mockImplementation(() => { 27 | throw new AzureDevOpsError('Custom error'); 28 | }), 29 | }; 30 | 31 | // Act & Assert 32 | await expect( 33 | updateWorkItem(mockConnection, 123, { title: 'Updated Title' }), 34 | ).rejects.toThrow(AzureDevOpsError); 35 | 36 | await expect( 37 | updateWorkItem(mockConnection, 123, { title: 'Updated Title' }), 38 | ).rejects.toThrow('Custom error'); 39 | }); 40 | 41 | test('should wrap unexpected errors in a friendly error message', async () => { 42 | // Arrange 43 | const mockConnection: any = { 44 | getWorkItemTrackingApi: jest.fn().mockImplementation(() => { 45 | throw new Error('Unexpected error'); 46 | }), 47 | }; 48 | 49 | // Act & Assert 50 | await expect( 51 | updateWorkItem(mockConnection, 123, { title: 'Updated Title' }), 52 | ).rejects.toThrow('Failed to update work item: Unexpected error'); 53 | }); 54 | }); 55 | ``` -------------------------------------------------------------------------------- /src/features/wikis/create-wiki/feature.spec.int.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { createWiki } from './feature'; 3 | import { WikiType } from './schema'; 4 | import { getTestConnection } from '@/shared/test/test-helpers'; 5 | import axios from 'axios'; 6 | 7 | axios.interceptors.request.use((request) => { 8 | console.log('Starting Request', JSON.stringify(request, null, 2)); 9 | return request; 10 | }); 11 | 12 | describe('createWiki (Integration)', () => { 13 | let connection: WebApi | null = null; 14 | let projectName: string; 15 | const testWikiName = `TestWiki_${new Date().getTime()}`; 16 | 17 | beforeAll(async () => { 18 | // Get a real connection using environment variables 19 | connection = await getTestConnection(); 20 | projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject'; 21 | }); 22 | 23 | test.skip('should create a project wiki', async () => { 24 | // PERMANENTLY SKIPPED: Azure DevOps only allows one wiki per project. 25 | // Running this test multiple times would fail after the first wiki is created. 26 | // This test is kept for reference but cannot be run repeatedly. 27 | 28 | // This connection must be available if we didn't skip 29 | if (!connection) { 30 | throw new Error( 31 | 'Connection should be available when test is not skipped', 32 | ); 33 | } 34 | 35 | // Create the wiki 36 | const wiki = await createWiki(connection, { 37 | name: testWikiName, 38 | projectId: projectName, 39 | type: WikiType.ProjectWiki, 40 | }); 41 | 42 | // Verify the wiki was created 43 | expect(wiki).toBeDefined(); 44 | expect(wiki.name).toBe(testWikiName); 45 | expect(wiki.projectId).toBe(projectName); 46 | expect(wiki.type).toBe(WikiType.ProjectWiki); 47 | }); 48 | 49 | // NOTE: We're not testing code wiki creation since that requires a repository 50 | // that would need to be created/cleaned up and is outside the scope of this test 51 | }); 52 | ``` -------------------------------------------------------------------------------- /src/features/wikis/list-wiki-pages/feature.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as azureDevOpsClient from '../../../clients/azure-devops'; 2 | import { AzureDevOpsError } from '../../../shared/errors/azure-devops-errors'; 3 | import { defaultOrg, defaultProject } from '../../../utils/environment'; 4 | import { ListWikiPagesOptions } from './schema'; 5 | 6 | /** 7 | * Summary information for a wiki page 8 | */ 9 | export interface WikiPageSummary { 10 | id: number; 11 | path: string; 12 | url?: string; 13 | order?: number; 14 | } 15 | 16 | /** 17 | * List wiki pages from a wiki 18 | * 19 | * @param options Options for listing wiki pages 20 | * @returns Array of wiki page summaries 21 | * @throws {AzureDevOpsResourceNotFoundError} When the wiki is not found 22 | * @throws {AzureDevOpsPermissionError} When the user does not have permission to access the wiki 23 | * @throws {AzureDevOpsError} When an error occurs while fetching the wiki pages 24 | */ 25 | export async function listWikiPages( 26 | options: ListWikiPagesOptions, 27 | ): Promise<WikiPageSummary[]> { 28 | const { organizationId, projectId, wikiId } = options; 29 | 30 | // Use defaults if not provided 31 | const orgId = organizationId || defaultOrg; 32 | const projId = projectId || defaultProject; 33 | 34 | try { 35 | // Create the client 36 | const client = await azureDevOpsClient.getWikiClient({ 37 | organizationId: orgId, 38 | }); 39 | 40 | // Get the wiki pages 41 | const pages = await client.listWikiPages(projId, wikiId); 42 | 43 | // Return the pages directly since the client interface now matches our requirements 44 | return pages.map((page) => ({ 45 | id: page.id, 46 | path: page.path, 47 | url: page.url, 48 | order: page.order, 49 | })); 50 | } catch (error) { 51 | // If it's already an AzureDevOpsError, rethrow it 52 | if (error instanceof AzureDevOpsError) { 53 | throw error; 54 | } 55 | // Otherwise wrap it in an AzureDevOpsError 56 | throw new AzureDevOpsError('Failed to list wiki pages', { cause: error }); 57 | } 58 | } 59 | ``` -------------------------------------------------------------------------------- /src/features/wikis/update-wiki-page/feature.spec.int.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { updateWikiPage } from './feature'; 3 | import { 4 | getTestConnection, 5 | shouldSkipIntegrationTest, 6 | } from '@/shared/test/test-helpers'; 7 | 8 | describe('updateWikiPage integration', () => { 9 | let connection: WebApi | null = null; 10 | let projectName: string; 11 | let organizationName: string; 12 | let wikiId: string; 13 | 14 | beforeAll(async () => { 15 | // Get a real connection using environment variables 16 | connection = await getTestConnection(); 17 | projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject'; 18 | organizationName = process.env.AZURE_DEVOPS_ORGANIZATION || ''; 19 | // Note: You'll need to set this to a valid wiki ID in your environment 20 | wikiId = `${projectName}.wiki`; 21 | }); 22 | 23 | test('should update a wiki page in Azure DevOps', async () => { 24 | // Skip if no connection is available 25 | if (shouldSkipIntegrationTest()) { 26 | return; 27 | } 28 | 29 | // This connection must be available if we didn't skip 30 | if (!connection) { 31 | throw new Error( 32 | 'Connection should be available when test is not skipped', 33 | ); 34 | } 35 | 36 | // Skip if no wiki ID is provided 37 | if (!wikiId) { 38 | console.log('Skipping test: No wiki ID provided'); 39 | return; 40 | } 41 | 42 | const testPagePath = '/test-page'; 43 | const testContent = '# Test Content\nThis is a test update.'; 44 | const testComment = 'Test update from integration test'; 45 | 46 | // Update the wiki page 47 | const result = await updateWikiPage({ 48 | organizationId: organizationName, 49 | projectId: projectName, 50 | wikiId: wikiId, 51 | pagePath: testPagePath, 52 | content: testContent, 53 | comment: testComment, 54 | }); 55 | 56 | // Verify the result 57 | expect(result).toBeDefined(); 58 | expect(result.path).toBe(testPagePath); 59 | expect(result.content).toBe(testContent); 60 | }); 61 | }); 62 | ``` -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Jest setup file that runs before all tests 3 | */ 4 | 5 | import dotenv from 'dotenv'; 6 | import path from 'path'; 7 | 8 | // Load environment variables from .env file 9 | // Use silent mode to prevent warning when .env file is not found 10 | const result = dotenv.config({ 11 | path: path.resolve(process.cwd(), '.env'), 12 | }); 13 | 14 | // Only log if .env file was successfully loaded and DEBUG=true 15 | if (!result.error && process.env.DEBUG === 'true') { 16 | console.log('Environment variables loaded from .env file'); 17 | } 18 | 19 | // Increase timeout for integration tests 20 | jest.setTimeout(30000); // 30 seconds 21 | 22 | // Suppress console output during tests unless specifically desired 23 | const originalConsoleLog = console.log; 24 | const originalConsoleWarn = console.warn; 25 | const originalConsoleError = console.error; 26 | 27 | if (process.env.DEBUG !== 'true') { 28 | global.console.log = (...args: any[]) => { 29 | if ( 30 | args[0]?.toString().includes('Skip') || 31 | args[0]?.toString().includes('Environment') 32 | ) { 33 | originalConsoleLog(...args); 34 | } 35 | }; 36 | 37 | global.console.warn = (...args: any[]) => { 38 | if (args[0]?.toString().includes('Warning')) { 39 | originalConsoleWarn(...args); 40 | } 41 | }; 42 | 43 | global.console.error = (...args: any[]) => { 44 | originalConsoleError(...args); 45 | }; 46 | } 47 | 48 | // Global setup before tests run 49 | beforeAll(() => { 50 | console.log('Starting tests with Testing Trophy approach...'); 51 | }); 52 | 53 | // Global cleanup after all tests 54 | afterAll(() => { 55 | console.log('All tests completed.'); 56 | }); 57 | 58 | // Clear all mocks before each test 59 | beforeEach(() => { 60 | jest.clearAllMocks(); 61 | jest.spyOn(console, 'log').mockImplementation(originalConsoleLog); 62 | jest.spyOn(console, 'warn').mockImplementation(originalConsoleWarn); 63 | jest.spyOn(console, 'error').mockImplementation(originalConsoleError); 64 | }); 65 | 66 | // Restore all mocks after each test 67 | afterEach(() => { 68 | jest.restoreAllMocks(); 69 | }); 70 | ``` -------------------------------------------------------------------------------- /src/features/work-items/create-work-item/feature.spec.unit.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { createWorkItem } from './feature'; 2 | import { AzureDevOpsError } from '../../../shared/errors'; 3 | 4 | // Unit tests should only focus on isolated logic 5 | // No real connections, HTTP requests, or dependencies 6 | describe('createWorkItem unit', () => { 7 | // Test for required title validation 8 | test('should throw error when title is not provided', async () => { 9 | // Arrange - mock connection, never used due to validation error 10 | const mockConnection: any = { 11 | getWorkItemTrackingApi: jest.fn(), 12 | }; 13 | 14 | // Act & Assert 15 | await expect( 16 | createWorkItem( 17 | mockConnection, 18 | 'TestProject', 19 | 'Task', 20 | { title: '' }, // Empty title 21 | ), 22 | ).rejects.toThrow('Title is required'); 23 | }); 24 | 25 | // Test for error propagation 26 | test('should propagate custom errors when thrown internally', async () => { 27 | // Arrange 28 | const mockConnection: any = { 29 | getWorkItemTrackingApi: jest.fn().mockImplementation(() => { 30 | throw new AzureDevOpsError('Custom error'); 31 | }), 32 | }; 33 | 34 | // Act & Assert 35 | await expect( 36 | createWorkItem(mockConnection, 'TestProject', 'Task', { 37 | title: 'Test Task', 38 | }), 39 | ).rejects.toThrow(AzureDevOpsError); 40 | 41 | await expect( 42 | createWorkItem(mockConnection, 'TestProject', 'Task', { 43 | title: 'Test Task', 44 | }), 45 | ).rejects.toThrow('Custom error'); 46 | }); 47 | 48 | test('should wrap unexpected errors in a friendly error message', async () => { 49 | // Arrange 50 | const mockConnection: any = { 51 | getWorkItemTrackingApi: jest.fn().mockImplementation(() => { 52 | throw new Error('Unexpected error'); 53 | }), 54 | }; 55 | 56 | // Act & Assert 57 | await expect( 58 | createWorkItem(mockConnection, 'TestProject', 'Task', { 59 | title: 'Test Task', 60 | }), 61 | ).rejects.toThrow('Failed to create work item: Unexpected error'); 62 | }); 63 | }); 64 | ``` -------------------------------------------------------------------------------- /src/features/wikis/get-wikis/feature.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { WikiV2 } from 'azure-devops-node-api/interfaces/WikiInterfaces'; 3 | import { 4 | AzureDevOpsError, 5 | AzureDevOpsResourceNotFoundError, 6 | } from '../../../shared/errors'; 7 | 8 | /** 9 | * Options for getting wikis 10 | */ 11 | export interface GetWikisOptions { 12 | /** 13 | * The ID or name of the organization 14 | * If not provided, the default organization will be used 15 | */ 16 | organizationId?: string; 17 | 18 | /** 19 | * The ID or name of the project 20 | * If not provided, the wikis from all projects will be returned 21 | */ 22 | projectId?: string; 23 | } 24 | 25 | /** 26 | * Get wikis in a project or organization 27 | * 28 | * @param connection The Azure DevOps WebApi connection 29 | * @param options Options for getting wikis 30 | * @returns List of wikis 31 | */ 32 | export async function getWikis( 33 | connection: WebApi, 34 | options: GetWikisOptions, 35 | ): Promise<WikiV2[]> { 36 | try { 37 | // Get the Wiki API client 38 | const wikiApi = await connection.getWikiApi(); 39 | 40 | // If a projectId is provided, get wikis for that specific project 41 | // Otherwise, get wikis for the entire organization 42 | const { projectId } = options; 43 | 44 | const wikis = await wikiApi.getAllWikis(projectId); 45 | 46 | return wikis || []; 47 | } catch (error) { 48 | // Handle resource not found errors specifically 49 | if ( 50 | error instanceof Error && 51 | error.message && 52 | error.message.includes('The resource cannot be found') 53 | ) { 54 | throw new AzureDevOpsResourceNotFoundError( 55 | `Resource not found: ${options.projectId ? `Project '${options.projectId}'` : 'Organization'}`, 56 | ); 57 | } 58 | 59 | // If it's already an AzureDevOpsError, rethrow it 60 | if (error instanceof AzureDevOpsError) { 61 | throw error; 62 | } 63 | 64 | // Otherwise, wrap it in a generic error 65 | throw new AzureDevOpsError( 66 | `Failed to get wikis: ${error instanceof Error ? error.message : String(error)}`, 67 | ); 68 | } 69 | } 70 | ``` -------------------------------------------------------------------------------- /src/features/pipelines/list-pipelines/feature.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { 3 | AzureDevOpsError, 4 | AzureDevOpsAuthenticationError, 5 | AzureDevOpsResourceNotFoundError, 6 | } from '../../../shared/errors'; 7 | import { ListPipelinesOptions, Pipeline } from '../types'; 8 | 9 | /** 10 | * List pipelines in a project 11 | * 12 | * @param connection The Azure DevOps WebApi connection 13 | * @param options Options for listing pipelines 14 | * @returns List of pipelines 15 | */ 16 | export async function listPipelines( 17 | connection: WebApi, 18 | options: ListPipelinesOptions, 19 | ): Promise<Pipeline[]> { 20 | try { 21 | const pipelinesApi = await connection.getPipelinesApi(); 22 | const { projectId, orderBy, top, continuationToken } = options; 23 | 24 | // Call the pipelines API to get the list of pipelines 25 | const pipelines = await pipelinesApi.listPipelines( 26 | projectId, 27 | orderBy, 28 | top, 29 | continuationToken, 30 | ); 31 | 32 | return pipelines; 33 | } catch (error) { 34 | // Handle specific error types 35 | if (error instanceof AzureDevOpsError) { 36 | throw error; 37 | } 38 | 39 | // Check for specific error types and convert to appropriate Azure DevOps errors 40 | if (error instanceof Error) { 41 | if ( 42 | error.message.includes('Authentication') || 43 | error.message.includes('Unauthorized') || 44 | error.message.includes('401') 45 | ) { 46 | throw new AzureDevOpsAuthenticationError( 47 | `Failed to authenticate: ${error.message}`, 48 | ); 49 | } 50 | 51 | if ( 52 | error.message.includes('not found') || 53 | error.message.includes('does not exist') || 54 | error.message.includes('404') 55 | ) { 56 | throw new AzureDevOpsResourceNotFoundError( 57 | `Project or resource not found: ${error.message}`, 58 | ); 59 | } 60 | } 61 | 62 | // Otherwise, wrap it in a generic error 63 | throw new AzureDevOpsError( 64 | `Failed to list pipelines: ${error instanceof Error ? error.message : String(error)}`, 65 | ); 66 | } 67 | } 68 | ``` -------------------------------------------------------------------------------- /src/features/repositories/get-repository/feature.spec.unit.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getRepository } from './feature'; 2 | import { 3 | AzureDevOpsError, 4 | AzureDevOpsResourceNotFoundError, 5 | } from '../../../shared/errors'; 6 | 7 | // Unit tests should only focus on isolated logic 8 | // No real connections, HTTP requests, or dependencies 9 | describe('getRepository unit', () => { 10 | test('should propagate resource not found errors', async () => { 11 | // Arrange 12 | const mockConnection: any = { 13 | getGitApi: jest.fn().mockImplementation(() => ({ 14 | getRepository: jest.fn().mockResolvedValue(null), // Simulate repository not found 15 | })), 16 | }; 17 | 18 | // Act & Assert 19 | await expect( 20 | getRepository(mockConnection, 'test-project', 'non-existent-repo'), 21 | ).rejects.toThrow(AzureDevOpsResourceNotFoundError); 22 | 23 | await expect( 24 | getRepository(mockConnection, 'test-project', 'non-existent-repo'), 25 | ).rejects.toThrow( 26 | "Repository 'non-existent-repo' not found in project 'test-project'", 27 | ); 28 | }); 29 | 30 | test('should propagate custom errors when thrown internally', async () => { 31 | // Arrange 32 | const mockConnection: any = { 33 | getGitApi: jest.fn().mockImplementation(() => { 34 | throw new AzureDevOpsError('Custom error'); 35 | }), 36 | }; 37 | 38 | // Act & Assert 39 | await expect( 40 | getRepository(mockConnection, 'test-project', 'test-repo'), 41 | ).rejects.toThrow(AzureDevOpsError); 42 | 43 | await expect( 44 | getRepository(mockConnection, 'test-project', 'test-repo'), 45 | ).rejects.toThrow('Custom error'); 46 | }); 47 | 48 | test('should wrap unexpected errors in a friendly error message', async () => { 49 | // Arrange 50 | const mockConnection: any = { 51 | getGitApi: jest.fn().mockImplementation(() => { 52 | throw new Error('Unexpected error'); 53 | }), 54 | }; 55 | 56 | // Act & Assert 57 | await expect( 58 | getRepository(mockConnection, 'test-project', 'test-repo'), 59 | ).rejects.toThrow('Failed to get repository: Unexpected error'); 60 | }); 61 | }); 62 | ``` -------------------------------------------------------------------------------- /docs/ci-setup.md: -------------------------------------------------------------------------------- ```markdown 1 | # CI Environment Setup for Integration Tests 2 | 3 | This document explains how to set up the CI environment to run integration tests with Azure DevOps. 4 | 5 | ## GitHub Secrets Configuration 6 | 7 | To run integration tests in the CI environment, you need to configure the following GitHub Secrets: 8 | 9 | 1. **AZURE_DEVOPS_ORG_URL**: The URL of your Azure DevOps organization (e.g., `https://dev.azure.com/your-organization`) 10 | 2. **AZURE_DEVOPS_PAT**: A Personal Access Token with appropriate permissions 11 | 3. **AZURE_DEVOPS_DEFAULT_PROJECT** (optional): The default project to use for tests 12 | 13 | ### Setting up GitHub Secrets 14 | 15 | 1. Go to your GitHub repository 16 | 2. Click on "Settings" > "Secrets and variables" > "Actions" 17 | 3. Click on "New repository secret" 18 | 4. Add each of the required secrets: 19 | 20 | #### AZURE_DEVOPS_ORG_URL 21 | 22 | - Name: `AZURE_DEVOPS_ORG_URL` 23 | - Value: `https://dev.azure.com/your-organization` 24 | 25 | #### AZURE_DEVOPS_PAT 26 | 27 | - Name: `AZURE_DEVOPS_PAT` 28 | - Value: Your Personal Access Token 29 | 30 | #### AZURE_DEVOPS_DEFAULT_PROJECT (optional) 31 | 32 | - Name: `AZURE_DEVOPS_DEFAULT_PROJECT` 33 | - Value: Your project name 34 | 35 | ## Personal Access Token (PAT) Requirements 36 | 37 | The PAT used for integration tests should have the following permissions: 38 | 39 | - **Code**: Read & Write 40 | - **Work Items**: Read & Write 41 | - **Build**: Read & Execute 42 | - **Project and Team**: Read 43 | - **Graph**: Read 44 | - **Release**: Read & Execute 45 | 46 | ## Security Considerations 47 | 48 | - Use a dedicated Azure DevOps organization or project for testing 49 | - Create a PAT with the minimum required permissions 50 | - Consider setting an expiration date for the PAT 51 | - Regularly rotate the PAT used in GitHub Secrets 52 | 53 | ## Troubleshooting 54 | 55 | If integration tests fail in CI: 56 | 57 | 1. Check the GitHub Actions logs for detailed error messages 58 | 2. Verify that the PAT has not expired 59 | 3. Ensure the PAT has the required permissions 60 | 4. Confirm that the organization URL is correct 61 | 5. Check if the default project exists and is accessible with the provided PAT 62 | ``` -------------------------------------------------------------------------------- /src/features/pull-requests/create-pull-request/feature.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { AzureDevOpsError } from '../../../shared/errors'; 3 | import { CreatePullRequestOptions, PullRequest } from '../types'; 4 | 5 | /** 6 | * Create a pull request 7 | * 8 | * @param connection The Azure DevOps WebApi connection 9 | * @param projectId The ID or name of the project 10 | * @param repositoryId The ID or name of the repository 11 | * @param options Options for creating the pull request 12 | * @returns The created pull request 13 | */ 14 | export async function createPullRequest( 15 | connection: WebApi, 16 | projectId: string, 17 | repositoryId: string, 18 | options: CreatePullRequestOptions, 19 | ): Promise<PullRequest> { 20 | try { 21 | if (!options.title) { 22 | throw new Error('Title is required'); 23 | } 24 | 25 | if (!options.sourceRefName) { 26 | throw new Error('Source branch is required'); 27 | } 28 | 29 | if (!options.targetRefName) { 30 | throw new Error('Target branch is required'); 31 | } 32 | 33 | const gitApi = await connection.getGitApi(); 34 | 35 | // Create the pull request object 36 | const pullRequest: PullRequest = { 37 | title: options.title, 38 | description: options.description, 39 | sourceRefName: options.sourceRefName, 40 | targetRefName: options.targetRefName, 41 | isDraft: options.isDraft || false, 42 | workItemRefs: options.workItemRefs?.map((id) => ({ 43 | id: id.toString(), 44 | })), 45 | reviewers: options.reviewers?.map((reviewer) => ({ 46 | id: reviewer, 47 | isRequired: true, 48 | })), 49 | ...options.additionalProperties, 50 | }; 51 | 52 | // Create the pull request 53 | const createdPullRequest = await gitApi.createPullRequest( 54 | pullRequest, 55 | repositoryId, 56 | projectId, 57 | ); 58 | 59 | if (!createdPullRequest) { 60 | throw new Error('Failed to create pull request'); 61 | } 62 | 63 | return createdPullRequest; 64 | } catch (error) { 65 | if (error instanceof AzureDevOpsError) { 66 | throw error; 67 | } 68 | throw new Error( 69 | `Failed to create pull request: ${error instanceof Error ? error.message : String(error)}`, 70 | ); 71 | } 72 | } 73 | ``` -------------------------------------------------------------------------------- /src/features/search/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from './schemas'; 2 | export * from './types'; 3 | export * from './search-code'; 4 | export * from './search-wiki'; 5 | export * from './search-work-items'; 6 | 7 | // Export tool definitions 8 | export * from './tool-definitions'; 9 | 10 | // New exports for request handling 11 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; 12 | import { WebApi } from 'azure-devops-node-api'; 13 | import { 14 | RequestIdentifier, 15 | RequestHandler, 16 | } from '../../shared/types/request-handler'; 17 | import { 18 | SearchCodeSchema, 19 | SearchWikiSchema, 20 | SearchWorkItemsSchema, 21 | searchCode, 22 | searchWiki, 23 | searchWorkItems, 24 | } from './'; 25 | 26 | /** 27 | * Checks if the request is for the search feature 28 | */ 29 | export const isSearchRequest: RequestIdentifier = ( 30 | request: CallToolRequest, 31 | ): boolean => { 32 | const toolName = request.params.name; 33 | return ['search_code', 'search_wiki', 'search_work_items'].includes(toolName); 34 | }; 35 | 36 | /** 37 | * Handles search feature requests 38 | */ 39 | export const handleSearchRequest: RequestHandler = async ( 40 | connection: WebApi, 41 | request: CallToolRequest, 42 | ): Promise<{ content: Array<{ type: string; text: string }> }> => { 43 | switch (request.params.name) { 44 | case 'search_code': { 45 | const args = SearchCodeSchema.parse(request.params.arguments); 46 | const result = await searchCode(connection, args); 47 | return { 48 | content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], 49 | }; 50 | } 51 | case 'search_wiki': { 52 | const args = SearchWikiSchema.parse(request.params.arguments); 53 | const result = await searchWiki(connection, args); 54 | return { 55 | content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], 56 | }; 57 | } 58 | case 'search_work_items': { 59 | const args = SearchWorkItemsSchema.parse(request.params.arguments); 60 | const result = await searchWorkItems(connection, args); 61 | return { 62 | content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], 63 | }; 64 | } 65 | default: 66 | throw new Error(`Unknown search tool: ${request.params.name}`); 67 | } 68 | }; 69 | ``` -------------------------------------------------------------------------------- /src/features/projects/get-project/feature.spec.int.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { getProject } from './feature'; 3 | import { 4 | getTestConnection, 5 | shouldSkipIntegrationTest, 6 | } from '@/shared/test/test-helpers'; 7 | 8 | describe('getProject integration', () => { 9 | let connection: WebApi | null = null; 10 | let projectName: string; 11 | 12 | beforeAll(async () => { 13 | // Get a real connection using environment variables 14 | connection = await getTestConnection(); 15 | projectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'DefaultProject'; 16 | }); 17 | 18 | test('should retrieve a real project from Azure DevOps', async () => { 19 | // Skip if no connection is available 20 | if (shouldSkipIntegrationTest()) { 21 | return; 22 | } 23 | 24 | // This connection must be available if we didn't skip 25 | if (!connection) { 26 | throw new Error( 27 | 'Connection should be available when test is not skipped', 28 | ); 29 | } 30 | 31 | // Act - make an actual API call to Azure DevOps 32 | const result = await getProject(connection, projectName); 33 | 34 | // Assert on the actual response 35 | expect(result).toBeDefined(); 36 | expect(result.name).toBe(projectName); 37 | expect(result.id).toBeDefined(); 38 | expect(result.url).toBeDefined(); 39 | expect(result.state).toBeDefined(); 40 | 41 | // Verify basic project structure 42 | expect(result.visibility).toBeDefined(); 43 | expect(result.lastUpdateTime).toBeDefined(); 44 | }); 45 | 46 | test('should throw error when project is not found', async () => { 47 | // Skip if no connection is available 48 | if (shouldSkipIntegrationTest()) { 49 | return; 50 | } 51 | 52 | // This connection must be available if we didn't skip 53 | if (!connection) { 54 | throw new Error( 55 | 'Connection should be available when test is not skipped', 56 | ); 57 | } 58 | 59 | // Use a non-existent project name 60 | const nonExistentProjectName = 'non-existent-project-' + Date.now(); 61 | 62 | // Act & Assert - should throw an error for non-existent project 63 | await expect( 64 | getProject(connection, nonExistentProjectName), 65 | ).rejects.toThrow(/not found|Failed to get project/); 66 | }); 67 | }); 68 | ``` -------------------------------------------------------------------------------- /src/features/organizations/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Re-export schemas and types 2 | export * from './schemas'; 3 | export * from './types'; 4 | 5 | // Re-export features 6 | export * from './list-organizations'; 7 | 8 | // Export tool definitions 9 | export * from './tool-definitions'; 10 | 11 | import { CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; 12 | import { WebApi } from 'azure-devops-node-api'; 13 | import { 14 | RequestIdentifier, 15 | RequestHandler, 16 | } from '../../shared/types/request-handler'; 17 | import { listOrganizations } from './list-organizations'; 18 | import { AzureDevOpsConfig } from '../../shared/types'; 19 | import { AuthenticationMethod } from '../../shared/auth'; 20 | 21 | /** 22 | * Checks if the request is for the organizations feature 23 | */ 24 | export const isOrganizationsRequest: RequestIdentifier = ( 25 | request: CallToolRequest, 26 | ): boolean => { 27 | const toolName = request.params.name; 28 | return ['list_organizations'].includes(toolName); 29 | }; 30 | 31 | /** 32 | * Handles organizations feature requests 33 | */ 34 | export const handleOrganizationsRequest: RequestHandler = async ( 35 | connection: WebApi, 36 | request: CallToolRequest, 37 | ): Promise<{ content: Array<{ type: string; text: string }> }> => { 38 | switch (request.params.name) { 39 | case 'list_organizations': { 40 | // Use environment variables for authentication method and PAT 41 | // This matches how other features handle authentication 42 | const config: AzureDevOpsConfig = { 43 | authMethod: 44 | process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 'pat' 45 | ? AuthenticationMethod.PersonalAccessToken 46 | : process.env.AZURE_DEVOPS_AUTH_METHOD?.toLowerCase() === 47 | 'azure-cli' 48 | ? AuthenticationMethod.AzureCli 49 | : AuthenticationMethod.AzureIdentity, 50 | personalAccessToken: process.env.AZURE_DEVOPS_PAT, 51 | organizationUrl: connection.serverUrl || '', 52 | }; 53 | 54 | const result = await listOrganizations(config); 55 | return { 56 | content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], 57 | }; 58 | } 59 | default: 60 | throw new Error(`Unknown organizations tool: ${request.params.name}`); 61 | } 62 | }; 63 | ``` -------------------------------------------------------------------------------- /src/features/pipelines/get-pipeline/feature.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { 3 | AzureDevOpsError, 4 | AzureDevOpsAuthenticationError, 5 | AzureDevOpsResourceNotFoundError, 6 | } from '../../../shared/errors'; 7 | import { GetPipelineOptions, Pipeline } from '../types'; 8 | 9 | /** 10 | * Get a specific pipeline by ID 11 | * 12 | * @param connection The Azure DevOps WebApi connection 13 | * @param options Options for getting a pipeline 14 | * @returns Pipeline details 15 | */ 16 | export async function getPipeline( 17 | connection: WebApi, 18 | options: GetPipelineOptions, 19 | ): Promise<Pipeline> { 20 | try { 21 | const pipelinesApi = await connection.getPipelinesApi(); 22 | const { projectId, pipelineId, pipelineVersion } = options; 23 | 24 | // Call the pipelines API to get the pipeline 25 | const pipeline = await pipelinesApi.getPipeline( 26 | projectId, 27 | pipelineId, 28 | pipelineVersion, 29 | ); 30 | 31 | // If pipeline not found, API returns null instead of throwing error 32 | if (pipeline === null) { 33 | throw new AzureDevOpsResourceNotFoundError( 34 | `Pipeline not found with ID: ${pipelineId}`, 35 | ); 36 | } 37 | 38 | return pipeline; 39 | } catch (error) { 40 | // Handle specific error types 41 | if (error instanceof AzureDevOpsError) { 42 | throw error; 43 | } 44 | 45 | // Check for specific error types and convert to appropriate Azure DevOps errors 46 | if (error instanceof Error) { 47 | if ( 48 | error.message.includes('Authentication') || 49 | error.message.includes('Unauthorized') || 50 | error.message.includes('401') 51 | ) { 52 | throw new AzureDevOpsAuthenticationError( 53 | `Failed to authenticate: ${error.message}`, 54 | ); 55 | } 56 | 57 | if ( 58 | error.message.includes('not found') || 59 | error.message.includes('does not exist') || 60 | error.message.includes('404') 61 | ) { 62 | throw new AzureDevOpsResourceNotFoundError( 63 | `Pipeline or project not found: ${error.message}`, 64 | ); 65 | } 66 | } 67 | 68 | // Otherwise, wrap it in a generic error 69 | throw new AzureDevOpsError( 70 | `Failed to get pipeline: ${error instanceof Error ? error.message : String(error)}`, 71 | ); 72 | } 73 | } 74 | ``` -------------------------------------------------------------------------------- /docs/tools/organizations.md: -------------------------------------------------------------------------------- ```markdown 1 | # Azure DevOps Organizations Tools 2 | 3 | This document describes the tools available for working with Azure DevOps organizations. 4 | 5 | ## list_organizations 6 | 7 | Lists all Azure DevOps organizations accessible to the authenticated user. 8 | 9 | ### Description 10 | 11 | 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. 12 | 13 | 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. 14 | 15 | ### Parameters 16 | 17 | This tool doesn't require any parameters. 18 | 19 | ```json 20 | { 21 | // No parameters required 22 | } 23 | ``` 24 | 25 | ### Response 26 | 27 | The tool returns an array of organization objects, each containing: 28 | 29 | - `id`: The unique identifier of the organization 30 | - `name`: The name of the organization 31 | - `url`: The URL of the organization 32 | 33 | Example response: 34 | 35 | ```json 36 | [ 37 | { 38 | "id": "org1-id", 39 | "name": "org1-name", 40 | "url": "https://dev.azure.com/org1-name" 41 | }, 42 | { 43 | "id": "org2-id", 44 | "name": "org2-name", 45 | "url": "https://dev.azure.com/org2-name" 46 | } 47 | ] 48 | ``` 49 | 50 | ### Error Handling 51 | 52 | The tool may throw the following errors: 53 | 54 | - `AzureDevOpsAuthenticationError`: If authentication fails or the user profile cannot be retrieved 55 | - General errors: If the accounts API call fails or other unexpected errors occur 56 | 57 | ### Example Usage 58 | 59 | ```typescript 60 | // Example MCP client call 61 | const result = await mcpClient.callTool('list_organizations', {}); 62 | console.log(result); 63 | ``` 64 | 65 | ### Implementation Details 66 | 67 | This tool uses a two-step process to retrieve organizations: 68 | 69 | 1. First, it gets the user profile from `https://app.vssps.visualstudio.com/_apis/profile/profiles/me` 70 | 2. Then it extracts the `publicAlias` from the profile response 71 | 3. Finally, it uses the `publicAlias` to get organizations from `https://app.vssps.visualstudio.com/_apis/accounts?memberId={publicAlias}` 72 | 73 | Authentication is handled using Basic Auth with the Personal Access Token. 74 | ``` -------------------------------------------------------------------------------- /src/features/projects/schemas.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from 'zod'; 2 | import { defaultProject, defaultOrg } from '../../utils/environment'; 3 | 4 | /** 5 | * Schema for getting a project 6 | */ 7 | export const GetProjectSchema = z.object({ 8 | projectId: z 9 | .string() 10 | .optional() 11 | .describe(`The ID or name of the project (Default: ${defaultProject})`), 12 | organizationId: z 13 | .string() 14 | .optional() 15 | .describe(`The ID or name of the organization (Default: ${defaultOrg})`), 16 | }); 17 | 18 | /** 19 | * Schema for getting detailed project information 20 | */ 21 | export const GetProjectDetailsSchema = z.object({ 22 | projectId: z 23 | .string() 24 | .optional() 25 | .describe(`The ID or name of the project (Default: ${defaultProject})`), 26 | organizationId: z 27 | .string() 28 | .optional() 29 | .describe(`The ID or name of the organization (Default: ${defaultOrg})`), 30 | includeProcess: z 31 | .boolean() 32 | .optional() 33 | .default(false) 34 | .describe('Include process information in the project result'), 35 | includeWorkItemTypes: z 36 | .boolean() 37 | .optional() 38 | .default(false) 39 | .describe('Include work item types and their structure'), 40 | includeFields: z 41 | .boolean() 42 | .optional() 43 | .default(false) 44 | .describe('Include field information for work item types'), 45 | includeTeams: z 46 | .boolean() 47 | .optional() 48 | .default(false) 49 | .describe('Include associated teams in the project result'), 50 | expandTeamIdentity: z 51 | .boolean() 52 | .optional() 53 | .default(false) 54 | .describe('Expand identity information in the team objects'), 55 | }); 56 | 57 | /** 58 | * Schema for listing projects 59 | */ 60 | export const ListProjectsSchema = z.object({ 61 | organizationId: z 62 | .string() 63 | .optional() 64 | .describe(`The ID or name of the organization (Default: ${defaultOrg})`), 65 | stateFilter: z 66 | .number() 67 | .optional() 68 | .describe( 69 | 'Filter on team project state (0: all, 1: well-formed, 2: creating, 3: deleting, 4: new)', 70 | ), 71 | top: z.number().optional().describe('Maximum number of projects to return'), 72 | skip: z.number().optional().describe('Number of projects to skip'), 73 | continuationToken: z 74 | .number() 75 | .optional() 76 | .describe('Gets the projects after the continuation token provided'), 77 | }); 78 | ``` -------------------------------------------------------------------------------- /src/features/repositories/list-repositories/feature.spec.unit.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { listRepositories } from './feature'; 2 | import { AzureDevOpsError } from '../../../shared/errors'; 3 | 4 | // Unit tests should only focus on isolated logic 5 | describe('listRepositories unit', () => { 6 | test('should return empty array when no repositories are found', async () => { 7 | // Arrange 8 | const mockConnection: any = { 9 | getGitApi: jest.fn().mockImplementation(() => ({ 10 | getRepositories: jest.fn().mockResolvedValue([]), // No repositories found 11 | })), 12 | }; 13 | 14 | // Act 15 | const result = await listRepositories(mockConnection, { 16 | projectId: 'test-project', 17 | }); 18 | 19 | // Assert 20 | expect(result).toEqual([]); 21 | }); 22 | 23 | test('should propagate custom errors when thrown internally', async () => { 24 | // Arrange 25 | const mockConnection: any = { 26 | getGitApi: jest.fn().mockImplementation(() => { 27 | throw new AzureDevOpsError('Custom error'); 28 | }), 29 | }; 30 | 31 | // Act & Assert 32 | await expect( 33 | listRepositories(mockConnection, { projectId: 'test-project' }), 34 | ).rejects.toThrow(AzureDevOpsError); 35 | 36 | await expect( 37 | listRepositories(mockConnection, { projectId: 'test-project' }), 38 | ).rejects.toThrow('Custom error'); 39 | }); 40 | 41 | test('should wrap unexpected errors in a friendly error message', async () => { 42 | // Arrange 43 | const mockConnection: any = { 44 | getGitApi: jest.fn().mockImplementation(() => { 45 | throw new Error('Unexpected error'); 46 | }), 47 | }; 48 | 49 | // Act & Assert 50 | await expect( 51 | listRepositories(mockConnection, { projectId: 'test-project' }), 52 | ).rejects.toThrow('Failed to list repositories: Unexpected error'); 53 | }); 54 | 55 | test('should respect the includeLinks option', async () => { 56 | // Arrange 57 | const mockGetRepositories = jest.fn().mockResolvedValue([]); 58 | const mockConnection: any = { 59 | getGitApi: jest.fn().mockImplementation(() => ({ 60 | getRepositories: mockGetRepositories, 61 | })), 62 | }; 63 | 64 | // Act 65 | await listRepositories(mockConnection, { 66 | projectId: 'test-project', 67 | includeLinks: true, 68 | }); 69 | 70 | // Assert 71 | expect(mockGetRepositories).toHaveBeenCalledWith('test-project', true); 72 | }); 73 | }); 74 | ``` -------------------------------------------------------------------------------- /src/utils/environment.spec.unit.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Mock the environment module before importing 2 | jest.mock('./environment', () => { 3 | const original = jest.requireActual('./environment'); 4 | return { 5 | ...original, 6 | // We'll keep getOrgNameFromUrl as is for its own tests 7 | getOrgNameFromUrl: original.getOrgNameFromUrl, 8 | }; 9 | }); 10 | 11 | import { getOrgNameFromUrl } from './environment'; 12 | 13 | describe('environment utilities', () => { 14 | // Store original environment variables 15 | const originalEnv = { ...process.env }; 16 | 17 | // Reset environment variables after each test 18 | afterEach(() => { 19 | process.env = { ...originalEnv }; 20 | jest.resetModules(); 21 | }); 22 | 23 | describe('getOrgNameFromUrl', () => { 24 | it('should extract organization name from Azure DevOps URL', () => { 25 | const url = 'https://dev.azure.com/test-organization'; 26 | expect(getOrgNameFromUrl(url)).toBe('test-organization'); 27 | }); 28 | 29 | it('should handle URLs with paths after the organization name', () => { 30 | const url = 'https://dev.azure.com/test-organization/project'; 31 | expect(getOrgNameFromUrl(url)).toBe('test-organization'); 32 | }); 33 | 34 | it('should return "unknown-organization" when URL is undefined', () => { 35 | expect(getOrgNameFromUrl(undefined)).toBe('unknown-organization'); 36 | }); 37 | 38 | it('should return "unknown-organization" when URL is empty', () => { 39 | expect(getOrgNameFromUrl('')).toBe('unknown-organization'); 40 | }); 41 | 42 | it('should return "unknown-organization" when URL does not match pattern', () => { 43 | const url = 'https://example.com/test-organization'; 44 | expect(getOrgNameFromUrl(url)).toBe('unknown-organization'); 45 | }); 46 | }); 47 | 48 | describe('defaultProject and defaultOrg', () => { 49 | // Since we can't easily test the environment variable initialization directly, 50 | // we'll test the getOrgNameFromUrl function which is used to derive defaultOrg 51 | 52 | it('should handle the real default case', () => { 53 | // This test is more of a documentation than a real test 54 | const orgNameFromUrl = getOrgNameFromUrl( 55 | process.env.AZURE_DEVOPS_ORG_URL, 56 | ); 57 | // We can't assert an exact value since it depends on the environment 58 | expect(typeof orgNameFromUrl).toBe('string'); 59 | }); 60 | }); 61 | }); 62 | ``` -------------------------------------------------------------------------------- /src/features/wikis/get-wiki-page/feature.spec.int.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { getWikiPage } from './feature'; 3 | import { getWikis } from '../get-wikis/feature'; 4 | import { 5 | getTestConnection, 6 | shouldSkipIntegrationTest, 7 | } from '@/shared/test/test-helpers'; 8 | import { getOrgNameFromUrl } from '@/utils/environment'; 9 | 10 | process.env.AZURE_DEVOPS_DEFAULT_PROJECT = 11 | process.env.AZURE_DEVOPS_DEFAULT_PROJECT || 'default-project'; 12 | 13 | describe('getWikiPage integration', () => { 14 | let connection: WebApi | null = null; 15 | let projectName: string; 16 | let orgUrl: string; 17 | 18 | beforeAll(async () => { 19 | // Mock the required environment variable for testing 20 | process.env.AZURE_DEVOPS_ORG_URL = 21 | process.env.AZURE_DEVOPS_ORG_URL || 'https://example.visualstudio.com'; 22 | // Get a real connection using environment variables 23 | connection = await getTestConnection(); 24 | 25 | // Get and validate required environment variables 26 | const envProjectName = process.env.AZURE_DEVOPS_DEFAULT_PROJECT; 27 | if (!envProjectName) { 28 | throw new Error( 29 | 'AZURE_DEVOPS_DEFAULT_PROJECT environment variable is required', 30 | ); 31 | } 32 | projectName = envProjectName; 33 | 34 | const envOrgUrl = process.env.AZURE_DEVOPS_ORG_URL; 35 | if (!envOrgUrl) { 36 | throw new Error('AZURE_DEVOPS_ORG_URL environment variable is required'); 37 | } 38 | orgUrl = envOrgUrl; 39 | }); 40 | 41 | test('should retrieve a wiki page', async () => { 42 | // Skip if no connection is available 43 | if (shouldSkipIntegrationTest()) { 44 | return; 45 | } 46 | 47 | // This connection must be available if we didn't skip 48 | if (!connection) { 49 | throw new Error( 50 | 'Connection should be available when test is not skipped', 51 | ); 52 | } 53 | 54 | // First get available wikis 55 | const wikis = await getWikis(connection, { projectId: projectName }); 56 | 57 | // Skip if no wikis are available 58 | if (wikis.length === 0) { 59 | console.log('Skipping test: No wikis available in the project'); 60 | return; 61 | } 62 | 63 | // Use the first available wiki 64 | const wiki = wikis[0]; 65 | if (!wiki.name) { 66 | throw new Error('Wiki name is undefined'); 67 | } 68 | 69 | // Get the wiki page 70 | const result = await getWikiPage({ 71 | organizationId: getOrgNameFromUrl(orgUrl), 72 | projectId: projectName, 73 | wikiId: wiki.name, 74 | pagePath: '/test', 75 | }); 76 | 77 | // Verify the result 78 | expect(result).toBeDefined(); 79 | expect(typeof result).toBe('string'); 80 | }); 81 | }); 82 | ``` -------------------------------------------------------------------------------- /src/features/projects/get-project/feature.spec.unit.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getProject } from './feature'; 2 | import { 3 | AzureDevOpsError, 4 | AzureDevOpsResourceNotFoundError, 5 | } from '../../../shared/errors'; 6 | import { TeamProject } from 'azure-devops-node-api/interfaces/CoreInterfaces'; 7 | import { WebApi } from 'azure-devops-node-api'; 8 | 9 | // Create a partial mock interface for ICoreApi 10 | interface MockCoreApi { 11 | getProject: jest.Mock<Promise<TeamProject | null>>; 12 | } 13 | 14 | // Create a mock connection that resembles WebApi with minimal implementation 15 | interface MockConnection { 16 | getCoreApi: jest.Mock<Promise<MockCoreApi>>; 17 | serverUrl?: string; 18 | authHandler?: unknown; 19 | rest?: unknown; 20 | vsoClient?: unknown; 21 | } 22 | 23 | // Unit tests should only focus on isolated logic 24 | describe('getProject unit', () => { 25 | test('should throw resource not found error when project is null', async () => { 26 | // Arrange 27 | const mockCoreApi: MockCoreApi = { 28 | getProject: jest.fn().mockResolvedValue(null), // Simulate project not found 29 | }; 30 | 31 | const mockConnection: MockConnection = { 32 | getCoreApi: jest.fn().mockResolvedValue(mockCoreApi), 33 | }; 34 | 35 | // Act & Assert 36 | await expect( 37 | getProject(mockConnection as unknown as WebApi, 'non-existent-project'), 38 | ).rejects.toThrow(AzureDevOpsResourceNotFoundError); 39 | 40 | await expect( 41 | getProject(mockConnection as unknown as WebApi, 'non-existent-project'), 42 | ).rejects.toThrow("Project 'non-existent-project' not found"); 43 | }); 44 | 45 | test('should propagate custom errors when thrown internally', async () => { 46 | // Arrange 47 | const mockConnection: MockConnection = { 48 | getCoreApi: jest.fn().mockImplementation(() => { 49 | throw new AzureDevOpsError('Custom error'); 50 | }), 51 | }; 52 | 53 | // Act & Assert 54 | await expect( 55 | getProject(mockConnection as unknown as WebApi, 'test-project'), 56 | ).rejects.toThrow(AzureDevOpsError); 57 | 58 | await expect( 59 | getProject(mockConnection as unknown as WebApi, 'test-project'), 60 | ).rejects.toThrow('Custom error'); 61 | }); 62 | 63 | test('should wrap unexpected errors in a friendly error message', async () => { 64 | // Arrange 65 | const mockConnection: MockConnection = { 66 | getCoreApi: jest.fn().mockImplementation(() => { 67 | throw new Error('Unexpected error'); 68 | }), 69 | }; 70 | 71 | // Act & Assert 72 | await expect( 73 | getProject(mockConnection as unknown as WebApi, 'test-project'), 74 | ).rejects.toThrow('Failed to get project: Unexpected error'); 75 | }); 76 | }); 77 | ```