# Directory Structure ``` ├── .github │ └── workflows │ └── ci.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── smithery.yaml ├── src │ ├── api │ │ ├── connection.ts │ │ └── wiki.ts │ ├── config │ │ └── environment.ts │ ├── errors.ts │ ├── index.ts │ └── tools │ ├── board │ │ ├── get.ts │ │ └── index.ts │ ├── pipeline │ │ ├── get.ts │ │ ├── index.ts │ │ └── trigger.ts │ ├── project │ │ ├── index.ts │ │ └── list.ts │ ├── pull-request │ │ ├── create.ts │ │ ├── get.ts │ │ ├── index.ts │ │ └── update.ts │ ├── wiki │ │ ├── create.ts │ │ ├── get.ts │ │ ├── index.ts │ │ └── update.ts │ └── work-item │ ├── create.ts │ ├── get.ts │ ├── index.ts │ ├── list.ts │ └── update.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Build output 8 | build/ 9 | dist/ 10 | *.tsbuildinfo 11 | 12 | # IDE and editor files 13 | .idea/ 14 | .vscode/ 15 | *.swp 16 | *.swo 17 | *~ 18 | 19 | # Environment variables 20 | .env 21 | .env.local 22 | .env.*.local 23 | 24 | # Operating System 25 | .DS_Store 26 | Thumbs.db ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Azure DevOps MCP Server for Cline 2 | [](https://smithery.ai/server/@stefanskiasan/azure-devops-mcp-server) 3 | 4 | This Model Context Protocol (MCP) server provides integration with Azure DevOps, allowing Cline to interact with Azure DevOps services. 5 | 6 | ## Prerequisites 7 | 8 | - Node.js (v20 LTS or higher) 9 | - npm (comes with Node.js) 10 | - A Cline installation 11 | - Azure DevOps account with access tokens 12 | 13 | ## Installation 14 | 15 | ### Installing via Smithery 16 | 17 | To install Azure DevOps Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@stefanskiasan/azure-devops-mcp-server): 18 | 19 | ```bash 20 | npx -y @smithery/cli install @stefanskiasan/azure-devops-mcp-server --client claude 21 | ``` 22 | 23 | ### Manual Installation 24 | 1. Clone this repository: 25 | ```bash 26 | git clone https://github.com/stefanskiasan/azure-devops-mcp-server.git 27 | cd azure-devops-mcp-server 28 | ``` 29 | 30 | 2. Install dependencies: 31 | ```bash 32 | npm install 33 | ``` 34 | 35 | 3. Build the server: 36 | ```bash 37 | npm run build 38 | ``` 39 | 40 | Note: The build output (`build/` directory) is not included in version control. You must run the build command after cloning the repository. 41 | 42 | ## Configuration 43 | 44 | ### 1. Get Azure DevOps Personal Access Token (PAT) 45 | 46 | 1. Go to Azure DevOps and sign in 47 | 2. Click on your profile picture in the top right 48 | 3. Select "Security" 49 | 4. Click "New Token" 50 | 5. Give your token a name and select the required scopes: 51 | - `Code (read, write)` - For Pull Request operations 52 | - `Work Items (read, write)` - For Work Item management 53 | - `Build (read, execute)` - For Pipeline operations 54 | - `Wiki (read, write)` - For Wiki operations 55 | - `Project and Team (read)` - For Project and Board information 56 | 6. Copy the generated token 57 | 58 | ### 2. Configure Cline MCP Settings 59 | 60 | Add the server configuration to your Cline MCP settings file: 61 | 62 | - For VSCode extension: `%APPDATA%/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json` 63 | - For Claude desktop app: `%LOCALAPPDATA%/Claude/claude_desktop_config.json` 64 | 65 | Add the following configuration to the `mcpServers` object: 66 | 67 | ```json 68 | { 69 | "mcpServers": { 70 | "azure-devops": { 71 | "command": "node", 72 | "args": ["/absolute/path/to/azure-devops-server/build/index.js"], 73 | "env": { 74 | "AZURE_DEVOPS_ORG": "your-organization", 75 | "AZURE_DEVOPS_PAT": "your-personal-access-token", 76 | "AZURE_DEVOPS_PROJECT": "your-project-name" 77 | }, 78 | "disabled": false, 79 | "autoApprove": [] 80 | } 81 | } 82 | } 83 | ``` 84 | 85 | Replace the following values: 86 | - `/absolute/path/to/azure-devops-server`: The absolute path to where you cloned this repository 87 | - `your-organization`: Your Azure DevOps organization name 88 | - `your-project-name`: Your Azure DevOps project name 89 | - `your-personal-access-token`: The PAT you generated in step 1 90 | 91 | ## Available Tools 92 | 93 | ### Work Items 94 | - `get_work_item`: Get a work item by ID 95 | - `list_work_items`: Query work items using WIQL 96 | - `create_work_item`: Create a new work item (Bug, Task, User Story) 97 | - `update_work_item`: Update an existing work item 98 | 99 | ### Boards 100 | - `get_boards`: Get available boards in the project 101 | 102 | ### Pipelines 103 | - `list_pipelines`: List all pipelines in the project 104 | - `trigger_pipeline`: Execute a pipeline 105 | 106 | ### Pull Requests 107 | - `list_pull_requests`: List pull requests 108 | - `create_pull_request`: Create a new pull request 109 | - `update_pull_request`: Update a pull request 110 | - `get_pull_request`: Get pull request details 111 | 112 | ### Wiki 113 | - `get_wikis`: List all wikis in the project 114 | - `get_wiki_page`: Get a wiki page 115 | - `create_wiki`: Create a new wiki 116 | - `update_wiki_page`: Create or update a wiki page 117 | 118 | ### Projects 119 | - `list_projects`: List all projects in the Azure DevOps organization 120 | 121 | ## Verification 122 | 123 | 1. Restart Cline (or VSCode) after adding the configuration 124 | 2. The Azure DevOps MCP server should now be listed in Cline's capabilities 125 | 3. You can verify the installation using the MCP Inspector: 126 | ```bash 127 | npm run inspector 128 | ``` 129 | 130 | ## Troubleshooting 131 | 132 | 1. If the server isn't connecting: 133 | - Check that the path in your MCP settings is correct 134 | - Verify your Azure DevOps credentials 135 | - Check the Cline logs for any error messages 136 | 137 | 2. If you get authentication errors: 138 | - Verify your PAT hasn't expired 139 | - Ensure the PAT has all necessary scopes 140 | - Double-check the organization and project names 141 | 142 | 3. For other issues: 143 | - Run the inspector tool to verify the server is working correctly 144 | - Check the server logs for any error messages 145 | 146 | ## Development 147 | 148 | To modify or extend the server: 149 | 150 | 1. Make your changes in the `src` directory 151 | 2. Run `npm run watch` for development 152 | 3. Build with `npm run build` when ready 153 | 4. Test using the inspector: `npm run inspector` 154 | 155 | ## License 156 | 157 | MIT License - See [LICENSE](LICENSE) for details 158 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } 16 | ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: '20' 18 | cache: 'npm' 19 | 20 | - name: Install dependencies 21 | run: npm ci 22 | 23 | - name: Build project 24 | run: npm run build ``` -------------------------------------------------------------------------------- /src/tools/project/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { listProjects } from './list.js'; 2 | import { AzureDevOpsConfig } from '../../config/environment.js'; 3 | 4 | const definitions = [ 5 | { 6 | name: 'list_projects', 7 | description: 'List all projects in the Azure DevOps organization', 8 | inputSchema: { 9 | type: 'object', 10 | properties: {}, 11 | required: [], 12 | }, 13 | }, 14 | ]; 15 | 16 | export const projectTools = { 17 | initialize: (config: AzureDevOpsConfig) => ({ 18 | listProjects: (args?: Record<string, unknown>) => listProjects(args, config), 19 | definitions, 20 | }), 21 | definitions, 22 | }; ``` -------------------------------------------------------------------------------- /src/tools/board/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getBoards } from './get.js'; 2 | import { AzureDevOpsConfig } from '../../config/environment.js'; 3 | 4 | const definitions = [ 5 | { 6 | name: 'get_boards', 7 | description: 'List available boards in the project', 8 | inputSchema: { 9 | type: 'object', 10 | properties: { 11 | team: { 12 | type: 'string', 13 | description: 'Team name (optional)', 14 | }, 15 | }, 16 | }, 17 | }, 18 | ]; 19 | 20 | export const boardTools = { 21 | initialize: (config: AzureDevOpsConfig) => ({ 22 | getBoards: (args: any) => getBoards(args, config), 23 | definitions, 24 | }), 25 | definitions, 26 | }; ``` -------------------------------------------------------------------------------- /src/tools/board/get.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { AzureDevOpsConnection } from '../../api/connection.js'; 2 | import { AzureDevOpsConfig } from '../../config/environment.js'; 3 | 4 | interface GetBoardsArgs { 5 | team?: string; 6 | } 7 | 8 | export async function getBoards(args: GetBoardsArgs, config: AzureDevOpsConfig) { 9 | AzureDevOpsConnection.initialize(config); 10 | const connection = AzureDevOpsConnection.getInstance(); 11 | const workApi = await connection.getWorkApi(); 12 | 13 | const teamContext = { 14 | project: config.project, 15 | team: args.team || `${config.project} Team`, 16 | }; 17 | 18 | const boards = await workApi.getBoards(teamContext); 19 | 20 | return { 21 | content: [ 22 | { 23 | type: 'text', 24 | text: JSON.stringify(boards, null, 2), 25 | }, 26 | ], 27 | }; 28 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "azure-devops-server", 3 | "license": "MIT", 4 | "version": "0.1.0", 5 | "description": "A Model Context Protocol server", 6 | "private": true, 7 | "type": "module", 8 | "bin": { 9 | "azure-devops-server": "./build/index.js" 10 | }, 11 | "files": [ 12 | "build" 13 | ], 14 | "scripts": { 15 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 16 | "prepare": "npm run build", 17 | "watch": "tsc --watch", 18 | "inspector": "npx @modelcontextprotocol/inspector build/index.js" 19 | }, 20 | "dependencies": { 21 | "@modelcontextprotocol/sdk": "0.6.0", 22 | "@types/node-fetch": "^2.6.12", 23 | "azure-devops-node-api": "^14.1.0", 24 | "node-fetch": "^2.7.0" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^20.11.24", 28 | "typescript": "^5.3.3" 29 | } 30 | } 31 | ``` -------------------------------------------------------------------------------- /src/tools/work-item/list.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { AzureDevOpsConnection } from '../../api/connection.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | import { Wiql } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js'; 5 | 6 | export async function listWorkItems(args: Wiql, config: AzureDevOpsConfig) { 7 | if (!args.query) { 8 | throw new McpError(ErrorCode.InvalidParams, 'Invalid WIQL query'); 9 | } 10 | 11 | AzureDevOpsConnection.initialize(config); 12 | const connection = AzureDevOpsConnection.getInstance(); 13 | const workItemTrackingApi = await connection.getWorkItemTrackingApi(); 14 | 15 | const queryResult = await workItemTrackingApi.queryByWiql( 16 | args, 17 | { project: config.project } 18 | ); 19 | 20 | return { 21 | content: [ 22 | { 23 | type: 'text', 24 | text: JSON.stringify(queryResult, null, 2), 25 | }, 26 | ], 27 | }; 28 | } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | # Start with a Node.js image with npm pre-installed 3 | FROM node:16-alpine AS builder 4 | 5 | # Create and set the working directory 6 | WORKDIR /app 7 | 8 | # Copy all necessary files 9 | COPY . . 10 | 11 | # Install the dependencies 12 | RUN npm install 13 | 14 | # Build the server 15 | RUN npm run build 16 | 17 | # Create a new image for the actual server 18 | FROM node:16-alpine 19 | 20 | # Set the working directory 21 | WORKDIR /app 22 | 23 | # Copy only the necessary files from the builder image 24 | COPY --from=builder /app/build /app/build 25 | COPY --from=builder /app/node_modules /app/node_modules 26 | COPY --from=builder /app/package.json /app/package.json 27 | 28 | # Define environment variables for Azure DevOps 29 | ENV AZURE_DEVOPS_ORG=your-organization 30 | ENV AZURE_DEVOPS_PROJECT=your-project 31 | ENV AZURE_DEVOPS_TOKEN=your-personal-access-token 32 | 33 | # Start the server 34 | ENTRYPOINT ["node", "build/index.js"] ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - azureDevOpsOrg 10 | - azureDevOpsProject 11 | - azureDevOpsPat 12 | properties: 13 | azureDevOpsOrg: 14 | type: string 15 | description: Your Azure DevOps organization name. 16 | azureDevOpsProject: 17 | type: string 18 | description: Your Azure DevOps project name. 19 | azureDevOpsPat: 20 | type: string 21 | description: Your Azure DevOps Personal Access Token. 22 | commandFunction: 23 | # A function that produces the CLI command to start the MCP on stdio. 24 | |- 25 | (config) => ({ command: 'node', args: ['build/index.js'], env: { AZURE_DEVOPS_ORG: config.azureDevOpsOrg, AZURE_DEVOPS_PROJECT: config.azureDevOpsProject, AZURE_DEVOPS_PAT: config.azureDevOpsPat } }) ``` -------------------------------------------------------------------------------- /src/tools/project/list.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { AzureDevOpsConnection } from '../../api/connection.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | 5 | export async function listProjects(args: Record<string, unknown> | undefined, config: AzureDevOpsConfig) { 6 | AzureDevOpsConnection.initialize(config); 7 | const connection = AzureDevOpsConnection.getInstance(); 8 | const coreApi = await connection.getCoreApi(); 9 | 10 | try { 11 | const projects = await coreApi.getProjects(); 12 | 13 | return { 14 | content: [ 15 | { 16 | type: 'text', 17 | text: JSON.stringify(projects, null, 2), 18 | }, 19 | ], 20 | }; 21 | } catch (error: unknown) { 22 | if (error instanceof McpError) throw error; 23 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 24 | throw new McpError( 25 | ErrorCode.InternalError, 26 | `Failed to list projects: ${errorMessage}` 27 | ); 28 | } 29 | } ``` -------------------------------------------------------------------------------- /src/tools/work-item/create.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { AzureDevOpsConnection } from '../../api/connection.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | import { JsonPatchOperation } from 'azure-devops-node-api/interfaces/common/VSSInterfaces.js'; 5 | 6 | export async function createWorkItem(args: { type: string; document: JsonPatchOperation[] }, config: AzureDevOpsConfig) { 7 | if (!args.type || !args.document || !args.document.length) { 8 | throw new McpError(ErrorCode.InvalidParams, 'Work item type and patch document are required'); 9 | } 10 | 11 | AzureDevOpsConnection.initialize(config); 12 | const connection = AzureDevOpsConnection.getInstance(); 13 | const workItemTrackingApi = await connection.getWorkItemTrackingApi(); 14 | 15 | const workItem = await workItemTrackingApi.createWorkItem( 16 | undefined, 17 | args.document, 18 | config.project, 19 | args.type 20 | ); 21 | 22 | return { 23 | content: [ 24 | { 25 | type: 'text', 26 | text: JSON.stringify(workItem, null, 2), 27 | }, 28 | ], 29 | }; 30 | } ``` -------------------------------------------------------------------------------- /src/tools/pipeline/get.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { AzureDevOpsConnection } from '../../api/connection.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | 5 | interface GetPipelinesArgs { 6 | folder?: string; 7 | name?: string; 8 | } 9 | 10 | export async function getPipelines(args: GetPipelinesArgs, config: AzureDevOpsConfig) { 11 | AzureDevOpsConnection.initialize(config); 12 | const connection = AzureDevOpsConnection.getInstance(); 13 | const pipelineApi = await connection.getBuildApi(); 14 | 15 | try { 16 | const pipelines = await pipelineApi.getDefinitions( 17 | config.project, 18 | args.name, 19 | args.folder 20 | ); 21 | 22 | return { 23 | content: [ 24 | { 25 | type: 'text', 26 | text: JSON.stringify(pipelines, null, 2), 27 | }, 28 | ], 29 | }; 30 | } catch (error: unknown) { 31 | if (error instanceof McpError) throw error; 32 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 33 | throw new McpError( 34 | ErrorCode.InternalError, 35 | `Failed to get pipelines: ${errorMessage}` 36 | ); 37 | } 38 | } ``` -------------------------------------------------------------------------------- /src/tools/work-item/get.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { AzureDevOpsConnection } from '../../api/connection.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | import { WorkItemBatchGetRequest, WorkItemExpand, WorkItemErrorPolicy } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js'; 5 | 6 | export async function getWorkItem(args: WorkItemBatchGetRequest, config: AzureDevOpsConfig) { 7 | if (!args.ids || !args.ids.length) { 8 | throw new McpError(ErrorCode.InvalidParams, 'Invalid work item ID'); 9 | } 10 | 11 | AzureDevOpsConnection.initialize(config); 12 | const connection = AzureDevOpsConnection.getInstance(); 13 | const workItemTrackingApi = await connection.getWorkItemTrackingApi(); 14 | const workItems = await workItemTrackingApi.getWorkItems( 15 | args.ids, 16 | args.fields || ['System.Id', 'System.Title', 'System.State', 'System.Description'], 17 | args.asOf, 18 | WorkItemExpand.All, 19 | args.errorPolicy, 20 | config.project 21 | ); 22 | 23 | return { 24 | content: [ 25 | { 26 | type: 'text', 27 | text: JSON.stringify(workItems, null, 2), 28 | }, 29 | ], 30 | }; 31 | } ``` -------------------------------------------------------------------------------- /src/tools/work-item/update.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { AzureDevOpsConnection } from '../../api/connection.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | import { JsonPatchOperation } from 'azure-devops-node-api/interfaces/common/VSSInterfaces.js'; 5 | import { WorkItemUpdate } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js'; 6 | 7 | export async function updateWorkItem(args: { id: number; document: JsonPatchOperation[] }, config: AzureDevOpsConfig) { 8 | if (!args.id || !args.document || !args.document.length) { 9 | throw new McpError(ErrorCode.InvalidParams, 'Work item ID and patch document are required'); 10 | } 11 | 12 | AzureDevOpsConnection.initialize(config); 13 | const connection = AzureDevOpsConnection.getInstance(); 14 | const workItemTrackingApi = await connection.getWorkItemTrackingApi(); 15 | 16 | const workItem = await workItemTrackingApi.updateWorkItem( 17 | undefined, 18 | args.document, 19 | args.id, 20 | config.project 21 | ); 22 | 23 | return { 24 | content: [ 25 | { 26 | type: 'text', 27 | text: JSON.stringify(workItem, null, 2), 28 | }, 29 | ], 30 | }; 31 | } ``` -------------------------------------------------------------------------------- /src/config/environment.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { env } from 'process'; 2 | import { ConfigurationError } from '../errors.js'; 3 | 4 | export interface AzureDevOpsConfig { 5 | pat: string; 6 | org: string; 7 | project: string; 8 | orgUrl: string; 9 | } 10 | 11 | function validateConfigValue(value: string | undefined, name: string): string { 12 | if (!value || value.trim() === '') { 13 | throw new ConfigurationError( 14 | `${name} is required and must be provided either through environment variables or constructor options` 15 | ); 16 | } 17 | return value.trim(); 18 | } 19 | 20 | export function createConfig(options?: Partial<AzureDevOpsConfig>): AzureDevOpsConfig { 21 | const PAT = validateConfigValue( 22 | options?.pat ?? env.AZURE_DEVOPS_PAT, 23 | 'Personal Access Token (pat)' 24 | ); 25 | const ORG = validateConfigValue( 26 | options?.org ?? env.AZURE_DEVOPS_ORG, 27 | 'Organization (org)' 28 | ); 29 | const PROJECT = validateConfigValue( 30 | options?.project ?? env.AZURE_DEVOPS_PROJECT, 31 | 'Project (project)' 32 | ); 33 | 34 | if (!ORG.match(/^[a-zA-Z0-9-_]+$/)) { 35 | throw new ConfigurationError( 36 | 'Organization name must contain only alphanumeric characters, hyphens, and underscores' 37 | ); 38 | } 39 | 40 | return { 41 | pat: PAT, 42 | org: ORG, 43 | project: PROJECT, 44 | orgUrl: `https://dev.azure.com/${ORG}`, 45 | }; 46 | } ``` -------------------------------------------------------------------------------- /src/api/connection.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as azdev from 'azure-devops-node-api'; 2 | import { WebApi } from 'azure-devops-node-api'; 3 | import { AzureDevOpsConfig } from '../config/environment.js'; 4 | import { WikiApi } from './wiki.js'; 5 | 6 | export class AzureDevOpsConnection { 7 | private static instance: WebApi | null = null; 8 | private static config: AzureDevOpsConfig; 9 | private static wikiApi: WikiApi | null = null; 10 | 11 | public static initialize(config: AzureDevOpsConfig): void { 12 | this.config = config; 13 | // Reset instances when config changes 14 | this.instance = null; 15 | this.wikiApi = null; 16 | } 17 | 18 | public static getInstance(): WebApi { 19 | if (!this.config) { 20 | throw new Error('AzureDevOpsConnection must be initialized with config before use'); 21 | } 22 | 23 | if (!this.instance) { 24 | const authHandler = azdev.getPersonalAccessTokenHandler(this.config.pat); 25 | this.instance = new azdev.WebApi(this.config.orgUrl, authHandler); 26 | } 27 | return this.instance; 28 | } 29 | 30 | public static getWikiApi(): WikiApi { 31 | if (!this.config) { 32 | throw new Error('AzureDevOpsConnection must be initialized with config before use'); 33 | } 34 | 35 | if (!this.wikiApi) { 36 | const connection = this.getInstance(); 37 | this.wikiApi = new WikiApi(connection, this.config); 38 | } 39 | return this.wikiApi; 40 | } 41 | } ``` -------------------------------------------------------------------------------- /src/tools/wiki/create.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { AzureDevOpsConnection } from '../../api/connection.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | import { WikiType } from 'azure-devops-node-api/interfaces/WikiInterfaces.js'; 5 | 6 | interface CreateWikiArgs { 7 | name: string; 8 | projectId?: string; 9 | mappedPath?: string; 10 | } 11 | 12 | export async function createWiki(args: CreateWikiArgs, config: AzureDevOpsConfig) { 13 | if (!args.name) { 14 | throw new McpError(ErrorCode.InvalidParams, 'Wiki name is required'); 15 | } 16 | 17 | AzureDevOpsConnection.initialize(config); 18 | const connection = AzureDevOpsConnection.getInstance(); 19 | const wikiApi = await connection.getWikiApi(); 20 | 21 | try { 22 | const wikiCreateParams = { 23 | name: args.name, 24 | projectId: args.projectId || config.project, 25 | mappedPath: args.mappedPath || '/', 26 | type: WikiType.ProjectWiki, 27 | }; 28 | 29 | const wiki = await wikiApi.createWiki(wikiCreateParams, config.project); 30 | 31 | return { 32 | content: [ 33 | { 34 | type: 'text', 35 | text: JSON.stringify(wiki, null, 2), 36 | }, 37 | ], 38 | }; 39 | } catch (error: unknown) { 40 | if (error instanceof McpError) throw error; 41 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 42 | throw new McpError( 43 | ErrorCode.InternalError, 44 | `Failed to create wiki: ${errorMessage}` 45 | ); 46 | } 47 | } ``` -------------------------------------------------------------------------------- /src/tools/pipeline/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getPipelines } from './get.js'; 2 | import { triggerPipeline } from './trigger.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | 5 | const definitions = [ 6 | { 7 | name: 'list_pipelines', 8 | description: 'List all pipelines in the project', 9 | inputSchema: { 10 | type: 'object', 11 | properties: { 12 | folder: { 13 | type: 'string', 14 | description: 'Filter pipelines by folder path (optional)', 15 | }, 16 | name: { 17 | type: 'string', 18 | description: 'Filter pipelines by name (optional)', 19 | }, 20 | }, 21 | }, 22 | }, 23 | { 24 | name: 'trigger_pipeline', 25 | description: 'Trigger a pipeline run', 26 | inputSchema: { 27 | type: 'object', 28 | properties: { 29 | pipelineId: { 30 | type: 'number', 31 | description: 'Pipeline ID to trigger', 32 | }, 33 | branch: { 34 | type: 'string', 35 | description: 'Branch to run the pipeline on (optional, defaults to default branch)', 36 | }, 37 | variables: { 38 | type: 'object', 39 | description: 'Pipeline variables to override (optional)', 40 | additionalProperties: { 41 | type: 'string', 42 | }, 43 | }, 44 | }, 45 | required: ['pipelineId'], 46 | }, 47 | }, 48 | ]; 49 | 50 | export const pipelineTools = { 51 | initialize: (config: AzureDevOpsConfig) => ({ 52 | getPipelines: (args: any) => getPipelines(args, config), 53 | triggerPipeline: (args: any) => triggerPipeline(args, config), 54 | definitions, 55 | }), 56 | definitions, 57 | }; ``` -------------------------------------------------------------------------------- /src/tools/pull-request/create.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { AzureDevOpsConnection } from '../../api/connection.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | import { GitPullRequest } from 'azure-devops-node-api/interfaces/GitInterfaces.js'; 5 | 6 | interface CreatePullRequestArgs { 7 | repositoryId: string; 8 | sourceRefName: string; 9 | targetRefName: string; 10 | title: string; 11 | description?: string; 12 | reviewers?: string[]; 13 | } 14 | 15 | export async function createPullRequest(args: CreatePullRequestArgs, config: AzureDevOpsConfig) { 16 | if (!args.repositoryId || !args.sourceRefName || !args.targetRefName || !args.title) { 17 | throw new McpError( 18 | ErrorCode.InvalidParams, 19 | 'Repository ID, source branch, target branch, and title are required' 20 | ); 21 | } 22 | 23 | AzureDevOpsConnection.initialize(config); 24 | const connection = AzureDevOpsConnection.getInstance(); 25 | const gitApi = await connection.getGitApi(); 26 | 27 | try { 28 | const pullRequestToCreate: GitPullRequest = { 29 | sourceRefName: args.sourceRefName, 30 | targetRefName: args.targetRefName, 31 | title: args.title, 32 | description: args.description, 33 | reviewers: args.reviewers?.map(id => ({ id })), 34 | }; 35 | 36 | const createdPr = await gitApi.createPullRequest( 37 | pullRequestToCreate, 38 | args.repositoryId, 39 | config.project 40 | ); 41 | 42 | return { 43 | content: [ 44 | { 45 | type: 'text', 46 | text: JSON.stringify(createdPr, null, 2), 47 | }, 48 | ], 49 | }; 50 | } catch (error: unknown) { 51 | if (error instanceof McpError) throw error; 52 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 53 | throw new McpError( 54 | ErrorCode.InternalError, 55 | `Failed to create pull request: ${errorMessage}` 56 | ); 57 | } 58 | } ``` -------------------------------------------------------------------------------- /src/tools/pull-request/get.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { AzureDevOpsConnection } from '../../api/connection.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces.js'; 5 | 6 | interface GetPullRequestsArgs { 7 | status?: 'active' | 'completed' | 'abandoned'; 8 | creatorId?: string; 9 | repositoryId?: string; 10 | } 11 | 12 | export async function getPullRequests(args: GetPullRequestsArgs, config: AzureDevOpsConfig) { 13 | AzureDevOpsConnection.initialize(config); 14 | const connection = AzureDevOpsConnection.getInstance(); 15 | const gitApi = await connection.getGitApi(); 16 | 17 | try { 18 | let statusFilter: PullRequestStatus | undefined; 19 | if (args.status) { 20 | switch (args.status) { 21 | case 'active': 22 | statusFilter = 1; // PullRequestStatus.Active 23 | break; 24 | case 'completed': 25 | statusFilter = 3; // PullRequestStatus.Completed 26 | break; 27 | case 'abandoned': 28 | statusFilter = 2; // PullRequestStatus.Abandoned 29 | break; 30 | } 31 | } 32 | 33 | const searchCriteria = { 34 | status: statusFilter, 35 | creatorId: args.creatorId, 36 | repositoryId: args.repositoryId, 37 | }; 38 | 39 | const pullRequests = await gitApi.getPullRequests( 40 | args.repositoryId || config.project, 41 | searchCriteria 42 | ); 43 | 44 | return { 45 | content: [ 46 | { 47 | type: 'text', 48 | text: JSON.stringify(pullRequests, null, 2), 49 | }, 50 | ], 51 | }; 52 | } catch (error: unknown) { 53 | if (error instanceof McpError) throw error; 54 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 55 | throw new McpError( 56 | ErrorCode.InternalError, 57 | `Failed to get pull requests: ${errorMessage}` 58 | ); 59 | } 60 | } ``` -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- ```typescript 1 | export class BaseError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | this.name = this.constructor.name; 5 | // Restore prototype chain in Node.js 6 | Object.setPrototypeOf(this, BaseError.prototype); 7 | } 8 | } 9 | 10 | export class ConfigurationError extends BaseError { 11 | constructor(message: string) { 12 | super(message); 13 | Object.setPrototypeOf(this, ConfigurationError.prototype); 14 | } 15 | } 16 | 17 | export class ApiError extends BaseError { 18 | constructor( 19 | message: string, 20 | public readonly statusCode?: number, 21 | public readonly response?: unknown 22 | ) { 23 | super(message); 24 | Object.setPrototypeOf(this, ApiError.prototype); 25 | } 26 | } 27 | 28 | export class WikiError extends ApiError { 29 | constructor( 30 | message: string, 31 | statusCode?: number, 32 | public readonly wikiId?: string, 33 | public readonly path?: string, 34 | response?: unknown 35 | ) { 36 | super(message, statusCode, response); 37 | Object.setPrototypeOf(this, WikiError.prototype); 38 | } 39 | } 40 | 41 | export class WikiNotFoundError extends WikiError { 42 | constructor(wikiId: string) { 43 | super(`Wiki with ID ${wikiId} not found`, 404, wikiId); 44 | Object.setPrototypeOf(this, WikiNotFoundError.prototype); 45 | } 46 | } 47 | 48 | export class WikiPageNotFoundError extends WikiError { 49 | constructor(wikiId: string, path: string) { 50 | super(`Wiki page not found at path ${path}`, 404, wikiId, path); 51 | Object.setPrototypeOf(this, WikiPageNotFoundError.prototype); 52 | } 53 | } 54 | 55 | export class AuthenticationError extends ApiError { 56 | constructor(message: string = 'Authentication failed') { 57 | super(message, 401); 58 | Object.setPrototypeOf(this, AuthenticationError.prototype); 59 | } 60 | } 61 | 62 | export class NotFoundError extends ApiError { 63 | constructor(message: string) { 64 | super(message, 404); 65 | Object.setPrototypeOf(this, NotFoundError.prototype); 66 | } 67 | } ``` -------------------------------------------------------------------------------- /src/tools/pipeline/trigger.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { AzureDevOpsConnection } from '../../api/connection.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | 5 | interface TriggerPipelineArgs { 6 | pipelineId: number; 7 | branch?: string; 8 | variables?: Record<string, string>; 9 | } 10 | 11 | export async function triggerPipeline(args: TriggerPipelineArgs, config: AzureDevOpsConfig) { 12 | if (!args.pipelineId) { 13 | throw new McpError(ErrorCode.InvalidParams, 'Pipeline ID is required'); 14 | } 15 | 16 | AzureDevOpsConnection.initialize(config); 17 | const connection = AzureDevOpsConnection.getInstance(); 18 | const pipelineApi = await connection.getBuildApi(); 19 | 20 | try { 21 | // Get pipeline definition first 22 | const definition = await pipelineApi.getDefinition( 23 | config.project, 24 | args.pipelineId 25 | ); 26 | 27 | if (!definition) { 28 | throw new McpError( 29 | ErrorCode.InvalidParams, 30 | `Pipeline with ID ${args.pipelineId} not found` 31 | ); 32 | } 33 | 34 | // Create build parameters 35 | const build = { 36 | definition: { 37 | id: args.pipelineId, 38 | }, 39 | project: definition.project, 40 | sourceBranch: args.branch || definition.repository?.defaultBranch || 'main', 41 | parameters: args.variables ? JSON.stringify(args.variables) : undefined, 42 | }; 43 | 44 | // Queue new build 45 | const queuedBuild = await pipelineApi.queueBuild(build, config.project); 46 | 47 | return { 48 | content: [ 49 | { 50 | type: 'text', 51 | text: JSON.stringify(queuedBuild, null, 2), 52 | }, 53 | ], 54 | }; 55 | } catch (error: unknown) { 56 | if (error instanceof McpError) throw error; 57 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 58 | throw new McpError( 59 | ErrorCode.InternalError, 60 | `Failed to trigger pipeline: ${errorMessage}` 61 | ); 62 | } 63 | } ``` -------------------------------------------------------------------------------- /src/tools/wiki/update.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { AzureDevOpsConnection } from '../../api/connection.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | 5 | interface UpdateWikiPageArgs { 6 | wikiIdentifier: string; 7 | path: string; 8 | content: string; 9 | comment?: string; 10 | } 11 | 12 | export async function updateWikiPage(args: UpdateWikiPageArgs, config: AzureDevOpsConfig) { 13 | if (!args.wikiIdentifier || !args.path || !args.content) { 14 | throw new McpError( 15 | ErrorCode.InvalidParams, 16 | 'Wiki identifier, page path, and content are required' 17 | ); 18 | } 19 | 20 | AzureDevOpsConnection.initialize(config); 21 | const connection = AzureDevOpsConnection.getInstance(); 22 | const wikiApi = await connection.getWikiApi(); 23 | 24 | try { 25 | const wiki = await wikiApi.getWiki(config.project, args.wikiIdentifier); 26 | if (!wiki || !wiki.id) { 27 | throw new McpError( 28 | ErrorCode.InvalidParams, 29 | `Wiki ${args.wikiIdentifier} not found` 30 | ); 31 | } 32 | 33 | const updateParams = { 34 | content: args.content, 35 | comment: args.comment || `Updated page ${args.path}`, 36 | }; 37 | 38 | // Da die Wiki-API keine direkte Methode zum Aktualisieren von Seiten bietet, 39 | // geben wir vorerst nur die Wiki-Informationen zurück 40 | return { 41 | content: [ 42 | { 43 | type: 'text', 44 | text: JSON.stringify({ 45 | wiki, 46 | path: args.path, 47 | message: 'Wiki page update is not supported in the current API version', 48 | requestedUpdate: updateParams 49 | }, null, 2), 50 | }, 51 | ], 52 | }; 53 | } catch (error: unknown) { 54 | if (error instanceof McpError) throw error; 55 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 56 | throw new McpError( 57 | ErrorCode.InternalError, 58 | `Failed to update wiki page: ${errorMessage}` 59 | ); 60 | } 61 | } ``` -------------------------------------------------------------------------------- /src/tools/wiki/get.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { AzureDevOpsConnection } from '../../api/connection.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | 5 | interface GetWikiPageArgs { 6 | wikiIdentifier: string; 7 | path: string; 8 | version?: string; 9 | includeContent?: boolean; 10 | } 11 | 12 | export async function getWikis(args: Record<string, never>, config: AzureDevOpsConfig) { 13 | AzureDevOpsConnection.initialize(config); 14 | const connection = AzureDevOpsConnection.getInstance(); 15 | const wikiApi = await connection.getWikiApi(); 16 | 17 | const wikis = await wikiApi.getAllWikis(config.project); 18 | 19 | return { 20 | content: [ 21 | { 22 | type: 'text', 23 | text: JSON.stringify(wikis, null, 2), 24 | }, 25 | ], 26 | }; 27 | } 28 | 29 | export async function getWikiPage(args: GetWikiPageArgs, config: AzureDevOpsConfig) { 30 | if (!args.wikiIdentifier || !args.path) { 31 | throw new McpError( 32 | ErrorCode.InvalidParams, 33 | 'Wiki identifier and page path are required' 34 | ); 35 | } 36 | 37 | AzureDevOpsConnection.initialize(config); 38 | const connection = AzureDevOpsConnection.getInstance(); 39 | const wikiApi = await connection.getWikiApi(); 40 | 41 | try { 42 | // Get wiki information 43 | const wiki = await wikiApi.getWiki(config.project, args.wikiIdentifier); 44 | if (!wiki || !wiki.id) { 45 | throw new McpError( 46 | ErrorCode.InvalidParams, 47 | `Wiki ${args.wikiIdentifier} not found` 48 | ); 49 | } 50 | 51 | // For now, we can only return the wiki information since the page API is not available 52 | return { 53 | content: [ 54 | { 55 | type: 'text', 56 | text: JSON.stringify({ 57 | id: wiki.id, 58 | name: wiki.name, 59 | path: args.path, 60 | message: 'Wiki page content retrieval is not supported in the current API version' 61 | }, null, 2), 62 | }, 63 | ], 64 | }; 65 | } catch (error: unknown) { 66 | if (error instanceof McpError) throw error; 67 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 68 | throw new McpError( 69 | ErrorCode.InternalError, 70 | `Failed to get wiki page: ${errorMessage}` 71 | ); 72 | } 73 | } ``` -------------------------------------------------------------------------------- /src/tools/wiki/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getWikis } from './get.js'; 2 | import { getWikiPage } from './get.js'; 3 | import { createWiki } from './create.js'; 4 | import { updateWikiPage } from './update.js'; 5 | import { AzureDevOpsConfig } from '../../config/environment.js'; 6 | 7 | const definitions = [ 8 | { 9 | name: 'get_wikis', 10 | description: 'List all wikis in the project', 11 | inputSchema: { 12 | type: 'object', 13 | properties: {}, 14 | }, 15 | }, 16 | { 17 | name: 'get_wiki_page', 18 | description: 'Get a wiki page by path', 19 | inputSchema: { 20 | type: 'object', 21 | properties: { 22 | wikiIdentifier: { 23 | type: 'string', 24 | description: 'Wiki identifier', 25 | }, 26 | path: { 27 | type: 'string', 28 | description: 'Page path', 29 | }, 30 | version: { 31 | type: 'string', 32 | description: 'Version (optional, defaults to main)', 33 | }, 34 | includeContent: { 35 | type: 'boolean', 36 | description: 'Include page content (optional, defaults to true)', 37 | }, 38 | }, 39 | required: ['wikiIdentifier', 'path'], 40 | }, 41 | }, 42 | { 43 | name: 'create_wiki', 44 | description: 'Create a new wiki', 45 | inputSchema: { 46 | type: 'object', 47 | properties: { 48 | name: { 49 | type: 'string', 50 | description: 'Wiki name', 51 | }, 52 | projectId: { 53 | type: 'string', 54 | description: 'Project ID (optional, defaults to current project)', 55 | }, 56 | mappedPath: { 57 | type: 'string', 58 | description: 'Mapped path (optional, defaults to /)', 59 | }, 60 | }, 61 | required: ['name'], 62 | }, 63 | }, 64 | { 65 | name: 'update_wiki_page', 66 | description: 'Create or update a wiki page', 67 | inputSchema: { 68 | type: 'object', 69 | properties: { 70 | wikiIdentifier: { 71 | type: 'string', 72 | description: 'Wiki identifier', 73 | }, 74 | path: { 75 | type: 'string', 76 | description: 'Page path', 77 | }, 78 | content: { 79 | type: 'string', 80 | description: 'Page content in markdown format', 81 | }, 82 | comment: { 83 | type: 'string', 84 | description: 'Comment for the update (optional)', 85 | }, 86 | }, 87 | required: ['wikiIdentifier', 'path', 'content'], 88 | }, 89 | }, 90 | ]; 91 | 92 | export const wikiTools = { 93 | initialize: (config: AzureDevOpsConfig) => ({ 94 | getWikis: (args: Record<string, never>) => getWikis(args, config), 95 | getWikiPage: (args: { wikiIdentifier: string; path: string; version?: string; includeContent?: boolean }) => 96 | getWikiPage(args, config), 97 | createWiki: (args: { name: string; projectId?: string; mappedPath?: string }) => 98 | createWiki(args, config), 99 | updateWikiPage: (args: { wikiIdentifier: string; path: string; content: string; comment?: string }) => 100 | updateWikiPage(args, config), 101 | definitions, 102 | }), 103 | definitions, 104 | }; ``` -------------------------------------------------------------------------------- /src/tools/pull-request/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getPullRequests } from './get.js'; 2 | import { createPullRequest } from './create.js'; 3 | import { updatePullRequest } from './update.js'; 4 | import { AzureDevOpsConfig } from '../../config/environment.js'; 5 | 6 | const definitions = [ 7 | { 8 | name: 'list_pull_requests', 9 | description: 'List all pull requests in the project', 10 | inputSchema: { 11 | type: 'object', 12 | properties: { 13 | status: { 14 | type: 'string', 15 | description: 'Filter by PR status (active, completed, abandoned)', 16 | enum: ['active', 'completed', 'abandoned'], 17 | }, 18 | creatorId: { 19 | type: 'string', 20 | description: 'Filter by creator ID (optional)', 21 | }, 22 | repositoryId: { 23 | type: 'string', 24 | description: 'Filter by repository ID (optional)', 25 | }, 26 | }, 27 | }, 28 | }, 29 | { 30 | name: 'create_pull_request', 31 | description: 'Create a new pull request', 32 | inputSchema: { 33 | type: 'object', 34 | properties: { 35 | repositoryId: { 36 | type: 'string', 37 | description: 'Repository ID', 38 | }, 39 | sourceRefName: { 40 | type: 'string', 41 | description: 'Source branch name (e.g. refs/heads/feature)', 42 | }, 43 | targetRefName: { 44 | type: 'string', 45 | description: 'Target branch name (e.g. refs/heads/main)', 46 | }, 47 | title: { 48 | type: 'string', 49 | description: 'Pull request title', 50 | }, 51 | description: { 52 | type: 'string', 53 | description: 'Pull request description', 54 | }, 55 | reviewers: { 56 | type: 'array', 57 | description: 'List of reviewer IDs (optional)', 58 | items: { 59 | type: 'string', 60 | }, 61 | }, 62 | }, 63 | required: ['repositoryId', 'sourceRefName', 'targetRefName', 'title'], 64 | }, 65 | }, 66 | { 67 | name: 'update_pull_request', 68 | description: 'Update an existing pull request', 69 | inputSchema: { 70 | type: 'object', 71 | properties: { 72 | pullRequestId: { 73 | type: 'number', 74 | description: 'Pull Request ID', 75 | }, 76 | status: { 77 | type: 'string', 78 | description: 'New status (active, abandoned, completed)', 79 | enum: ['active', 'abandoned', 'completed'], 80 | }, 81 | title: { 82 | type: 'string', 83 | description: 'New title (optional)', 84 | }, 85 | description: { 86 | type: 'string', 87 | description: 'New description (optional)', 88 | }, 89 | mergeStrategy: { 90 | type: 'string', 91 | description: 'Merge strategy (optional)', 92 | enum: ['squash', 'rebase', 'merge'], 93 | }, 94 | }, 95 | required: ['pullRequestId'], 96 | }, 97 | }, 98 | ]; 99 | 100 | export const pullRequestTools = { 101 | initialize: (config: AzureDevOpsConfig) => ({ 102 | getPullRequests: (args: any) => getPullRequests(args, config), 103 | createPullRequest: (args: any) => createPullRequest(args, config), 104 | updatePullRequest: (args: any) => updatePullRequest(args, config), 105 | definitions, 106 | }), 107 | definitions, 108 | }; ``` -------------------------------------------------------------------------------- /src/tools/pull-request/update.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { AzureDevOpsConnection } from '../../api/connection.js'; 3 | import { AzureDevOpsConfig } from '../../config/environment.js'; 4 | import { 5 | GitPullRequest, 6 | PullRequestStatus, 7 | GitPullRequestMergeStrategy 8 | } from 'azure-devops-node-api/interfaces/GitInterfaces.js'; 9 | 10 | interface UpdatePullRequestArgs { 11 | pullRequestId: number; 12 | status?: 'active' | 'abandoned' | 'completed'; 13 | title?: string; 14 | description?: string; 15 | mergeStrategy?: 'squash' | 'rebase' | 'merge'; 16 | } 17 | 18 | export async function updatePullRequest(args: UpdatePullRequestArgs, config: AzureDevOpsConfig) { 19 | if (!args.pullRequestId) { 20 | throw new McpError(ErrorCode.InvalidParams, 'Pull Request ID is required'); 21 | } 22 | 23 | AzureDevOpsConnection.initialize(config); 24 | const connection = AzureDevOpsConnection.getInstance(); 25 | const gitApi = await connection.getGitApi(); 26 | 27 | try { 28 | // Get current PR 29 | const currentPr = await gitApi.getPullRequestById(args.pullRequestId, config.project); 30 | if (!currentPr) { 31 | throw new McpError( 32 | ErrorCode.InvalidParams, 33 | `Pull Request with ID ${args.pullRequestId} not found` 34 | ); 35 | } 36 | 37 | if (!currentPr.repository?.id) { 38 | throw new McpError( 39 | ErrorCode.InvalidParams, 40 | `Repository information not found for PR ${args.pullRequestId}` 41 | ); 42 | } 43 | 44 | // Prepare update 45 | const prUpdate: GitPullRequest = { 46 | ...currentPr, 47 | title: args.title || currentPr.title, 48 | description: args.description || currentPr.description, 49 | }; 50 | 51 | // Handle status changes 52 | if (args.status) { 53 | switch (args.status) { 54 | case 'active': 55 | prUpdate.status = 1 as PullRequestStatus; // Active 56 | break; 57 | case 'abandoned': 58 | prUpdate.status = 2 as PullRequestStatus; // Abandoned 59 | break; 60 | case 'completed': 61 | prUpdate.status = 3 as PullRequestStatus; // Completed 62 | if (args.mergeStrategy) { 63 | let mergeStrategyValue: GitPullRequestMergeStrategy; 64 | switch (args.mergeStrategy) { 65 | case 'squash': 66 | mergeStrategyValue = 3; // GitPullRequestMergeStrategy.Squash 67 | break; 68 | case 'rebase': 69 | mergeStrategyValue = 2; // GitPullRequestMergeStrategy.Rebase 70 | break; 71 | case 'merge': 72 | mergeStrategyValue = 1; // GitPullRequestMergeStrategy.NoFastForward 73 | break; 74 | default: 75 | mergeStrategyValue = 1; // Default to no-fast-forward 76 | } 77 | 78 | prUpdate.completionOptions = { 79 | mergeStrategy: mergeStrategyValue, 80 | deleteSourceBranch: true, 81 | squashMerge: args.mergeStrategy === 'squash', 82 | }; 83 | } 84 | break; 85 | } 86 | } 87 | 88 | // Update PR 89 | const updatedPr = await gitApi.updatePullRequest( 90 | prUpdate, 91 | currentPr.repository.id, 92 | args.pullRequestId, 93 | config.project 94 | ); 95 | 96 | return { 97 | content: [ 98 | { 99 | type: 'text', 100 | text: JSON.stringify(updatedPr, null, 2), 101 | }, 102 | ], 103 | }; 104 | } catch (error: unknown) { 105 | if (error instanceof McpError) throw error; 106 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 107 | throw new McpError( 108 | ErrorCode.InternalError, 109 | `Failed to update pull request: ${errorMessage}` 110 | ); 111 | } 112 | } ``` -------------------------------------------------------------------------------- /src/tools/work-item/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { getWorkItem } from './get.js'; 2 | import { listWorkItems } from './list.js'; 3 | import { createWorkItem } from './create.js'; 4 | import { updateWorkItem } from './update.js'; 5 | import { AzureDevOpsConfig } from '../../config/environment.js'; 6 | import type { WorkItem, WorkItemBatchGetRequest, Wiql } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js'; 7 | import type { JsonPatchOperation } from 'azure-devops-node-api/interfaces/common/VSSInterfaces.js'; 8 | 9 | const definitions = [ 10 | { 11 | name: 'get_work_item', 12 | description: 'Get work items by IDs', 13 | inputSchema: { 14 | type: 'object', 15 | properties: { 16 | ids: { 17 | type: 'array', 18 | items: { 19 | type: 'number' 20 | }, 21 | description: 'Work item IDs', 22 | }, 23 | fields: { 24 | type: 'array', 25 | items: { 26 | type: 'string' 27 | }, 28 | description: 'Fields to include (e.g., "System.Title", "System.State")', 29 | }, 30 | asOf: { 31 | type: 'string', 32 | format: 'date-time', 33 | description: 'As of a specific date (ISO 8601)', 34 | }, 35 | $expand: { 36 | type: 'number', 37 | enum: [0, 1, 2, 3, 4], 38 | description: 'Expand options (None=0, Relations=1, Fields=2, Links=3, All=4)', 39 | }, 40 | errorPolicy: { 41 | type: 'number', 42 | enum: [1, 2], 43 | description: 'Error policy (Fail=1, Omit=2)', 44 | } 45 | }, 46 | required: ['ids'], 47 | }, 48 | }, 49 | { 50 | name: 'list_work_items', 51 | description: 'List work items from a board', 52 | inputSchema: { 53 | type: 'object', 54 | properties: { 55 | query: { 56 | type: 'string', 57 | description: 'WIQL query to filter work items', 58 | }, 59 | }, 60 | required: ['query'], 61 | }, 62 | }, 63 | { 64 | name: 'create_work_item', 65 | description: 'Create a new work item using JSON patch operations', 66 | inputSchema: { 67 | type: 'object', 68 | properties: { 69 | type: { 70 | type: 'string', 71 | description: 'Work item type (e.g., "Bug", "Task", "User Story")', 72 | }, 73 | document: { 74 | type: 'array', 75 | items: { 76 | type: 'object', 77 | properties: { 78 | op: { 79 | type: 'string', 80 | enum: ['add', 'remove', 'replace', 'move', 'copy', 'test'], 81 | description: 'The patch operation to perform', 82 | }, 83 | path: { 84 | type: 'string', 85 | description: 'The path for the operation (e.g., /fields/System.Title)', 86 | }, 87 | value: { 88 | description: 'The value for the operation', 89 | }, 90 | }, 91 | required: ['op', 'path'], 92 | }, 93 | description: 'Array of JSON patch operations to apply', 94 | }, 95 | }, 96 | required: ['type', 'document'], 97 | }, 98 | }, 99 | { 100 | name: 'update_work_item', 101 | description: 'Update an existing work item using JSON patch operations', 102 | inputSchema: { 103 | type: 'object', 104 | properties: { 105 | id: { 106 | type: 'number', 107 | description: 'ID of the work item to update', 108 | }, 109 | document: { 110 | type: 'array', 111 | items: { 112 | type: 'object', 113 | properties: { 114 | op: { 115 | type: 'string', 116 | enum: ['add', 'remove', 'replace', 'move', 'copy', 'test'], 117 | description: 'The patch operation to perform', 118 | }, 119 | path: { 120 | type: 'string', 121 | description: 'The path for the operation (e.g., /fields/System.Title)', 122 | }, 123 | value: { 124 | description: 'The value for the operation', 125 | }, 126 | }, 127 | required: ['op', 'path'], 128 | }, 129 | description: 'Array of JSON patch operations to apply', 130 | }, 131 | }, 132 | required: ['id', 'document'], 133 | }, 134 | }, 135 | ]; 136 | 137 | export const workItemTools = { 138 | initialize: (config: AzureDevOpsConfig) => ({ 139 | getWorkItem: (args: WorkItemBatchGetRequest) => getWorkItem(args, config), 140 | listWorkItems: (args: Wiql) => listWorkItems(args, config), 141 | createWorkItem: (args: { type: string; document: JsonPatchOperation[] }) => createWorkItem(args, config), 142 | updateWorkItem: (args: { id: number; document: JsonPatchOperation[] }) => updateWorkItem(args, config), 143 | definitions, 144 | }), 145 | definitions, 146 | }; ``` -------------------------------------------------------------------------------- /src/api/wiki.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { WebApi } from 'azure-devops-node-api'; 2 | import { AzureDevOpsConfig } from '../config/environment.js'; 3 | import { WikiError, WikiNotFoundError, WikiPageNotFoundError } from '../errors.js'; 4 | import fetch from 'node-fetch'; 5 | import type { Wiki, WikiPage, WikiPageResponse, WikiType, WikiCreateParameters, WikiPageCreateOrUpdateParameters } from 'azure-devops-node-api/interfaces/WikiInterfaces.js'; 6 | 7 | interface WikiListResponse { 8 | count: number; 9 | value: Wiki[]; 10 | } 11 | 12 | interface WikiCreateResponse extends WikiCreateParameters { 13 | id: string; 14 | createdBy: { 15 | id: string; 16 | displayName: string; 17 | uniqueName: string; 18 | }; 19 | createdDate: string; 20 | } 21 | 22 | interface WikiPageUpdateResponse extends WikiPageResponse { 23 | lastUpdatedBy: { 24 | id: string; 25 | displayName: string; 26 | uniqueName: string; 27 | }; 28 | lastUpdatedDate: string; 29 | } 30 | 31 | export class WikiApi { 32 | private connection: WebApi; 33 | private baseUrl: string; 34 | private config: AzureDevOpsConfig; 35 | 36 | constructor(connection: WebApi, config: AzureDevOpsConfig) { 37 | this.connection = connection; 38 | this.config = config; 39 | this.baseUrl = `${config.orgUrl}/${config.project}/_apis/wiki`; 40 | } 41 | 42 | private async getAuthHeader(): Promise<string> { 43 | const token = Buffer.from(`:${this.config.pat}`).toString('base64'); 44 | return `Basic ${token}`; 45 | } 46 | 47 | async createWiki(name: string, projectId?: string, mappedPath?: string): Promise<WikiCreateResponse> { 48 | const authHeader = await this.getAuthHeader(); 49 | const response = await fetch(`${this.baseUrl}?api-version=7.0`, { 50 | method: 'POST', 51 | headers: { 52 | 'Content-Type': 'application/json', 53 | Authorization: authHeader, 54 | }, 55 | body: JSON.stringify({ 56 | name, 57 | projectId: projectId || this.config.project, 58 | type: 'projectWiki', 59 | mappedPath: mappedPath || '/', 60 | }), 61 | }); 62 | 63 | if (!response.ok) { 64 | throw new WikiError( 65 | `Failed to create wiki: ${response.statusText}`, 66 | response.status, 67 | undefined, 68 | undefined, 69 | await response.text() 70 | ); 71 | } 72 | 73 | return response.json(); 74 | } 75 | 76 | async getAllWikis(): Promise<WikiListResponse> { 77 | const authHeader = await this.getAuthHeader(); 78 | const response = await fetch(`${this.baseUrl}?api-version=7.0`, { 79 | headers: { 80 | Authorization: authHeader, 81 | }, 82 | }); 83 | 84 | if (!response.ok) { 85 | throw new WikiError( 86 | `Failed to get wikis: ${response.statusText}`, 87 | response.status, 88 | undefined, 89 | undefined, 90 | await response.text() 91 | ); 92 | } 93 | 94 | return response.json(); 95 | } 96 | 97 | async getWikiPage(wikiIdentifier: string, path: string): Promise<WikiPage> { 98 | const authHeader = await this.getAuthHeader(); 99 | const encodedPath = encodeURIComponent(path); 100 | const response = await fetch( 101 | `${this.baseUrl}/${wikiIdentifier}/pages?path=${encodedPath}&api-version=7.0`, 102 | { 103 | headers: { 104 | Authorization: authHeader, 105 | }, 106 | } 107 | ); 108 | 109 | if (response.status === 404) { 110 | if (response.statusText.includes('Wiki not found')) { 111 | throw new WikiNotFoundError(wikiIdentifier); 112 | } 113 | throw new WikiPageNotFoundError(wikiIdentifier, path); 114 | } 115 | 116 | if (!response.ok) { 117 | throw new WikiError( 118 | `Failed to get wiki page: ${response.statusText}`, 119 | response.status, 120 | wikiIdentifier, 121 | path, 122 | await response.text() 123 | ); 124 | } 125 | 126 | return response.json(); 127 | } 128 | 129 | async updateWikiPage( 130 | wikiIdentifier: string, 131 | path: string, 132 | content: string, 133 | comment?: string 134 | ): Promise<WikiPageUpdateResponse> { 135 | const authHeader = await this.getAuthHeader(); 136 | const encodedPath = encodeURIComponent(path); 137 | const response = await fetch( 138 | `${this.baseUrl}/${wikiIdentifier}/pages?path=${encodedPath}&api-version=7.0`, 139 | { 140 | method: 'PUT', 141 | headers: { 142 | 'Content-Type': 'application/json', 143 | Authorization: authHeader, 144 | }, 145 | body: JSON.stringify({ 146 | content, 147 | comment: comment || `Updated page ${path}`, 148 | }), 149 | } 150 | ); 151 | 152 | if (response.status === 404) { 153 | if (response.statusText.includes('Wiki not found')) { 154 | throw new WikiNotFoundError(wikiIdentifier); 155 | } 156 | throw new WikiPageNotFoundError(wikiIdentifier, path); 157 | } 158 | 159 | if (!response.ok) { 160 | throw new WikiError( 161 | `Failed to update wiki page: ${response.statusText}`, 162 | response.status, 163 | wikiIdentifier, 164 | path, 165 | await response.text() 166 | ); 167 | } 168 | 169 | return response.json(); 170 | } 171 | } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { 5 | CallToolRequestSchema, 6 | ErrorCode, 7 | ListToolsRequestSchema, 8 | McpError, 9 | JSONRPCResponseSchema, 10 | JSONRPCResponse 11 | } from '@modelcontextprotocol/sdk/types.js'; 12 | 13 | // Import all tools 14 | import { workItemTools } from './tools/work-item/index.js'; 15 | import { boardTools } from './tools/board/index.js'; 16 | import { wikiTools } from './tools/wiki/index.js'; 17 | import { projectTools } from './tools/project/index.js'; 18 | import { pipelineTools } from './tools/pipeline/index.js'; 19 | import { pullRequestTools } from './tools/pull-request/index.js'; 20 | import { AzureDevOpsConfig, createConfig } from './config/environment.js'; 21 | 22 | import { ListToolsResult } from '@modelcontextprotocol/sdk/types.js'; 23 | 24 | // TODO: Use the proper ToolDefinition type from MCP SDK 25 | // type ToolDefinition = ReturnType<typeof workItemTools.initialize>['definitions'][number]; 26 | type ToolDefinition = any; 27 | 28 | // TODO: Use a proper ToolInstace definition from MCP SDK 29 | //interface ToolInstances { 30 | // workItem: ReturnType<typeof workItemTools.initialize>; 31 | // board: ReturnType<typeof boardTools.initialize>; 32 | // wiki: ReturnType<typeof wikiTools.initialize>; 33 | // project: ReturnType<typeof projectTools.initialize>; 34 | // pipeline: ReturnType<typeof pipelineTools.initialize>; 35 | // pullRequest: ReturnType<typeof pullRequestTools.initialize>; 36 | //} 37 | type ToolInstances = any; 38 | 39 | // Type Validations 40 | function validateArgs<T>(args: Record<string, unknown> | undefined, errorMessage: string): T { 41 | if (!args) { 42 | throw new McpError(ErrorCode.InvalidParams, errorMessage); 43 | } 44 | return args as T; 45 | } 46 | 47 | type MCPResponse = JSONRPCResponse["result"] 48 | 49 | // Response Formatting 50 | function formatResponse(data: unknown): MCPResponse { 51 | if (data && typeof data === 'object' && 'content' in data) { 52 | return data as MCPResponse; 53 | } 54 | return { 55 | content: [ 56 | { 57 | type: 'text', 58 | text: JSON.stringify(data, null, 2), 59 | }, 60 | ], 61 | }; 62 | } 63 | 64 | class AzureDevOpsServer { 65 | private server: Server; 66 | private config: AzureDevOpsConfig; 67 | private toolDefinitions: ToolDefinition[]; 68 | 69 | constructor(options?: Partial<Omit<AzureDevOpsConfig, 'orgUrl'>>) { 70 | this.config = createConfig(options); 71 | 72 | // Initialize tools with config 73 | const toolInstances = { 74 | workItem: workItemTools.initialize(this.config), 75 | board: boardTools.initialize(this.config), 76 | wiki: wikiTools.initialize(this.config), 77 | project: projectTools.initialize(this.config), 78 | pipeline: pipelineTools.initialize(this.config), 79 | pullRequest: pullRequestTools.initialize(this.config), 80 | }; 81 | 82 | // Combine all tool definitions 83 | this.toolDefinitions = [ 84 | ...toolInstances.workItem.definitions, 85 | ...toolInstances.board.definitions, 86 | ...toolInstances.wiki.definitions, 87 | ...toolInstances.project.definitions, 88 | ...toolInstances.pipeline.definitions, 89 | ...toolInstances.pullRequest.definitions, 90 | ]; 91 | 92 | this.server = new Server( 93 | { 94 | name: 'azure-devops-server', 95 | version: '0.1.0', 96 | }, 97 | { 98 | capabilities: { 99 | tools: {}, 100 | }, 101 | } 102 | ); 103 | 104 | this.setupToolHandlers(toolInstances); 105 | 106 | // Error handling 107 | this.server.onerror = (error) => console.error('[MCP Error]', error); 108 | process.on('SIGINT', async () => { 109 | await this.server.close(); 110 | process.exit(0); 111 | }); 112 | } 113 | 114 | private setupToolHandlers(tools: ToolInstances) { 115 | // List available tools 116 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 117 | tools: this.toolDefinitions, 118 | })); 119 | 120 | // Handle tool calls 121 | this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { 122 | try { 123 | let result; 124 | switch (request.params.name) { 125 | // Work Item Tools 126 | case 'get_work_item': 127 | result = await tools.workItem.getWorkItem(request.params.arguments); 128 | break; 129 | case 'list_work_items': 130 | result = await tools.workItem.listWorkItems(request.params.arguments); 131 | break; 132 | 133 | // Board Tools 134 | case 'get_boards': 135 | result = await tools.board.getBoards(request.params.arguments); 136 | break; 137 | 138 | // Wiki Tools 139 | case 'get_wikis': 140 | result = await tools.wiki.getWikis(request.params.arguments); 141 | break; 142 | case 'get_wiki_page': 143 | result = await tools.wiki.getWikiPage(request.params.arguments); 144 | break; 145 | case 'create_wiki': 146 | result = await tools.wiki.createWiki(request.params.arguments); 147 | break; 148 | case 'update_wiki_page': 149 | result = await tools.wiki.updateWikiPage(request.params.arguments); 150 | break; 151 | 152 | // Project Tools 153 | case 'list_projects': 154 | result = await tools.project.listProjects(request.params.arguments); 155 | break; 156 | 157 | // Pipeline Tools 158 | case 'list_pipelines': 159 | result = await tools.pipeline.getPipelines( 160 | validateArgs(request.params.arguments, 'Pipeline arguments required') 161 | ); 162 | break; 163 | case 'trigger_pipeline': 164 | result = await tools.pipeline.triggerPipeline( 165 | validateArgs(request.params.arguments, 'Pipeline trigger arguments required') 166 | ); 167 | break; 168 | 169 | // Pull Request Tools 170 | case 'list_pull_requests': 171 | result = await tools.pullRequest.getPullRequests( 172 | validateArgs(request.params.arguments, 'Pull request list arguments required') 173 | ); 174 | break; 175 | case 'get_pull_request': 176 | result = await tools.pullRequest.getPullRequest( 177 | validateArgs(request.params.arguments, 'Pull request ID required') 178 | ); 179 | break; 180 | case 'create_pull_request': 181 | result = await tools.pullRequest.createPullRequest( 182 | validateArgs(request.params.arguments, 'Pull request creation arguments required') 183 | ); 184 | break; 185 | case 'update_pull_request': 186 | result = await tools.pullRequest.updatePullRequest( 187 | validateArgs(request.params.arguments, 'Pull request update arguments required') 188 | ); 189 | break; 190 | 191 | default: 192 | throw new McpError( 193 | ErrorCode.MethodNotFound, 194 | `Unknown tool: ${request.params.name}` 195 | ); 196 | } 197 | 198 | // Ensure consistent response format 199 | const response = formatResponse(result); 200 | return { 201 | _meta: request.params._meta, 202 | ...response 203 | }; 204 | } catch (error: unknown) { 205 | if (error instanceof McpError) throw error; 206 | const errorMessage = error instanceof Error ? error.message : 'Unknown error'; 207 | throw new McpError( 208 | ErrorCode.InternalError, 209 | `Azure DevOps API error: ${errorMessage}` 210 | ); 211 | } 212 | }); 213 | } 214 | 215 | async run() { 216 | const transport = new StdioServerTransport(); 217 | await this.server.connect(transport); 218 | console.error('Azure DevOps MCP server running on stdio'); 219 | } 220 | } 221 | 222 | // Allow configuration through constructor or environment variables 223 | const server = new AzureDevOpsServer(); 224 | server.run().catch(console.error); 225 | ```