# 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: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* # Build output build/ dist/ *.tsbuildinfo # IDE and editor files .idea/ .vscode/ *.swp *.swo *~ # Environment variables .env .env.local .env.*.local # Operating System .DS_Store Thumbs.db ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Azure DevOps MCP Server for Cline [](https://smithery.ai/server/@stefanskiasan/azure-devops-mcp-server) This Model Context Protocol (MCP) server provides integration with Azure DevOps, allowing Cline to interact with Azure DevOps services. ## Prerequisites - Node.js (v20 LTS or higher) - npm (comes with Node.js) - A Cline installation - Azure DevOps account with access tokens ## Installation ### Installing via Smithery To install Azure DevOps Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@stefanskiasan/azure-devops-mcp-server): ```bash npx -y @smithery/cli install @stefanskiasan/azure-devops-mcp-server --client claude ``` ### Manual Installation 1. Clone this repository: ```bash git clone https://github.com/stefanskiasan/azure-devops-mcp-server.git cd azure-devops-mcp-server ``` 2. Install dependencies: ```bash npm install ``` 3. Build the server: ```bash npm run build ``` Note: The build output (`build/` directory) is not included in version control. You must run the build command after cloning the repository. ## Configuration ### 1. Get Azure DevOps Personal Access Token (PAT) 1. Go to Azure DevOps and sign in 2. Click on your profile picture in the top right 3. Select "Security" 4. Click "New Token" 5. Give your token a name and select the required scopes: - `Code (read, write)` - For Pull Request operations - `Work Items (read, write)` - For Work Item management - `Build (read, execute)` - For Pipeline operations - `Wiki (read, write)` - For Wiki operations - `Project and Team (read)` - For Project and Board information 6. Copy the generated token ### 2. Configure Cline MCP Settings Add the server configuration to your Cline MCP settings file: - For VSCode extension: `%APPDATA%/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json` - For Claude desktop app: `%LOCALAPPDATA%/Claude/claude_desktop_config.json` Add the following configuration to the `mcpServers` object: ```json { "mcpServers": { "azure-devops": { "command": "node", "args": ["/absolute/path/to/azure-devops-server/build/index.js"], "env": { "AZURE_DEVOPS_ORG": "your-organization", "AZURE_DEVOPS_PAT": "your-personal-access-token", "AZURE_DEVOPS_PROJECT": "your-project-name" }, "disabled": false, "autoApprove": [] } } } ``` Replace the following values: - `/absolute/path/to/azure-devops-server`: The absolute path to where you cloned this repository - `your-organization`: Your Azure DevOps organization name - `your-project-name`: Your Azure DevOps project name - `your-personal-access-token`: The PAT you generated in step 1 ## Available Tools ### Work Items - `get_work_item`: Get a work item by ID - `list_work_items`: Query work items using WIQL - `create_work_item`: Create a new work item (Bug, Task, User Story) - `update_work_item`: Update an existing work item ### Boards - `get_boards`: Get available boards in the project ### Pipelines - `list_pipelines`: List all pipelines in the project - `trigger_pipeline`: Execute a pipeline ### Pull Requests - `list_pull_requests`: List pull requests - `create_pull_request`: Create a new pull request - `update_pull_request`: Update a pull request - `get_pull_request`: Get pull request details ### Wiki - `get_wikis`: List all wikis in the project - `get_wiki_page`: Get a wiki page - `create_wiki`: Create a new wiki - `update_wiki_page`: Create or update a wiki page ### Projects - `list_projects`: List all projects in the Azure DevOps organization ## Verification 1. Restart Cline (or VSCode) after adding the configuration 2. The Azure DevOps MCP server should now be listed in Cline's capabilities 3. You can verify the installation using the MCP Inspector: ```bash npm run inspector ``` ## Troubleshooting 1. If the server isn't connecting: - Check that the path in your MCP settings is correct - Verify your Azure DevOps credentials - Check the Cline logs for any error messages 2. If you get authentication errors: - Verify your PAT hasn't expired - Ensure the PAT has all necessary scopes - Double-check the organization and project names 3. For other issues: - Run the inspector tool to verify the server is working correctly - Check the server logs for any error messages ## Development To modify or extend the server: 1. Make your changes in the `src` directory 2. Run `npm run watch` for development 3. Build with `npm run build` when ready 4. Test using the inspector: `npm run inspector` ## License MIT License - See [LICENSE](LICENSE) for details ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules"] } ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml name: CI on: pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Build project run: npm run build ``` -------------------------------------------------------------------------------- /src/tools/project/index.ts: -------------------------------------------------------------------------------- ```typescript import { listProjects } from './list.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; const definitions = [ { name: 'list_projects', description: 'List all projects in the Azure DevOps organization', inputSchema: { type: 'object', properties: {}, required: [], }, }, ]; export const projectTools = { initialize: (config: AzureDevOpsConfig) => ({ listProjects: (args?: Record<string, unknown>) => listProjects(args, config), definitions, }), definitions, }; ``` -------------------------------------------------------------------------------- /src/tools/board/index.ts: -------------------------------------------------------------------------------- ```typescript import { getBoards } from './get.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; const definitions = [ { name: 'get_boards', description: 'List available boards in the project', inputSchema: { type: 'object', properties: { team: { type: 'string', description: 'Team name (optional)', }, }, }, }, ]; export const boardTools = { initialize: (config: AzureDevOpsConfig) => ({ getBoards: (args: any) => getBoards(args, config), definitions, }), definitions, }; ``` -------------------------------------------------------------------------------- /src/tools/board/get.ts: -------------------------------------------------------------------------------- ```typescript import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; interface GetBoardsArgs { team?: string; } export async function getBoards(args: GetBoardsArgs, config: AzureDevOpsConfig) { AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const workApi = await connection.getWorkApi(); const teamContext = { project: config.project, team: args.team || `${config.project} Team`, }; const boards = await workApi.getBoards(teamContext); return { content: [ { type: 'text', text: JSON.stringify(boards, null, 2), }, ], }; } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "azure-devops-server", "license": "MIT", "version": "0.1.0", "description": "A Model Context Protocol server", "private": true, "type": "module", "bin": { "azure-devops-server": "./build/index.js" }, "files": [ "build" ], "scripts": { "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", "prepare": "npm run build", "watch": "tsc --watch", "inspector": "npx @modelcontextprotocol/inspector build/index.js" }, "dependencies": { "@modelcontextprotocol/sdk": "0.6.0", "@types/node-fetch": "^2.6.12", "azure-devops-node-api": "^14.1.0", "node-fetch": "^2.7.0" }, "devDependencies": { "@types/node": "^20.11.24", "typescript": "^5.3.3" } } ``` -------------------------------------------------------------------------------- /src/tools/work-item/list.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; import { Wiql } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js'; export async function listWorkItems(args: Wiql, config: AzureDevOpsConfig) { if (!args.query) { throw new McpError(ErrorCode.InvalidParams, 'Invalid WIQL query'); } AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const workItemTrackingApi = await connection.getWorkItemTrackingApi(); const queryResult = await workItemTrackingApi.queryByWiql( args, { project: config.project } ); return { content: [ { type: 'text', text: JSON.stringify(queryResult, null, 2), }, ], }; } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile # Start with a Node.js image with npm pre-installed FROM node:16-alpine AS builder # Create and set the working directory WORKDIR /app # Copy all necessary files COPY . . # Install the dependencies RUN npm install # Build the server RUN npm run build # Create a new image for the actual server FROM node:16-alpine # Set the working directory WORKDIR /app # Copy only the necessary files from the builder image COPY --from=builder /app/build /app/build COPY --from=builder /app/node_modules /app/node_modules COPY --from=builder /app/package.json /app/package.json # Define environment variables for Azure DevOps ENV AZURE_DEVOPS_ORG=your-organization ENV AZURE_DEVOPS_PROJECT=your-project ENV AZURE_DEVOPS_TOKEN=your-personal-access-token # Start the server ENTRYPOINT ["node", "build/index.js"] ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml startCommand: type: stdio configSchema: # JSON Schema defining the configuration options for the MCP. type: object required: - azureDevOpsOrg - azureDevOpsProject - azureDevOpsPat properties: azureDevOpsOrg: type: string description: Your Azure DevOps organization name. azureDevOpsProject: type: string description: Your Azure DevOps project name. azureDevOpsPat: type: string description: Your Azure DevOps Personal Access Token. commandFunction: # A function that produces the CLI command to start the MCP on stdio. |- (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 import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; export async function listProjects(args: Record<string, unknown> | undefined, config: AzureDevOpsConfig) { AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const coreApi = await connection.getCoreApi(); try { const projects = await coreApi.getProjects(); return { content: [ { type: 'text', text: JSON.stringify(projects, null, 2), }, ], }; } catch (error: unknown) { if (error instanceof McpError) throw error; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new McpError( ErrorCode.InternalError, `Failed to list projects: ${errorMessage}` ); } } ``` -------------------------------------------------------------------------------- /src/tools/work-item/create.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; import { JsonPatchOperation } from 'azure-devops-node-api/interfaces/common/VSSInterfaces.js'; export async function createWorkItem(args: { type: string; document: JsonPatchOperation[] }, config: AzureDevOpsConfig) { if (!args.type || !args.document || !args.document.length) { throw new McpError(ErrorCode.InvalidParams, 'Work item type and patch document are required'); } AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const workItemTrackingApi = await connection.getWorkItemTrackingApi(); const workItem = await workItemTrackingApi.createWorkItem( undefined, args.document, config.project, args.type ); return { content: [ { type: 'text', text: JSON.stringify(workItem, null, 2), }, ], }; } ``` -------------------------------------------------------------------------------- /src/tools/pipeline/get.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; interface GetPipelinesArgs { folder?: string; name?: string; } export async function getPipelines(args: GetPipelinesArgs, config: AzureDevOpsConfig) { AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const pipelineApi = await connection.getBuildApi(); try { const pipelines = await pipelineApi.getDefinitions( config.project, args.name, args.folder ); return { content: [ { type: 'text', text: JSON.stringify(pipelines, null, 2), }, ], }; } catch (error: unknown) { if (error instanceof McpError) throw error; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new McpError( ErrorCode.InternalError, `Failed to get pipelines: ${errorMessage}` ); } } ``` -------------------------------------------------------------------------------- /src/tools/work-item/get.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; import { WorkItemBatchGetRequest, WorkItemExpand, WorkItemErrorPolicy } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js'; export async function getWorkItem(args: WorkItemBatchGetRequest, config: AzureDevOpsConfig) { if (!args.ids || !args.ids.length) { throw new McpError(ErrorCode.InvalidParams, 'Invalid work item ID'); } AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const workItemTrackingApi = await connection.getWorkItemTrackingApi(); const workItems = await workItemTrackingApi.getWorkItems( args.ids, args.fields || ['System.Id', 'System.Title', 'System.State', 'System.Description'], args.asOf, WorkItemExpand.All, args.errorPolicy, config.project ); return { content: [ { type: 'text', text: JSON.stringify(workItems, null, 2), }, ], }; } ``` -------------------------------------------------------------------------------- /src/tools/work-item/update.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; import { JsonPatchOperation } from 'azure-devops-node-api/interfaces/common/VSSInterfaces.js'; import { WorkItemUpdate } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js'; export async function updateWorkItem(args: { id: number; document: JsonPatchOperation[] }, config: AzureDevOpsConfig) { if (!args.id || !args.document || !args.document.length) { throw new McpError(ErrorCode.InvalidParams, 'Work item ID and patch document are required'); } AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const workItemTrackingApi = await connection.getWorkItemTrackingApi(); const workItem = await workItemTrackingApi.updateWorkItem( undefined, args.document, args.id, config.project ); return { content: [ { type: 'text', text: JSON.stringify(workItem, null, 2), }, ], }; } ``` -------------------------------------------------------------------------------- /src/config/environment.ts: -------------------------------------------------------------------------------- ```typescript import { env } from 'process'; import { ConfigurationError } from '../errors.js'; export interface AzureDevOpsConfig { pat: string; org: string; project: string; orgUrl: string; } function validateConfigValue(value: string | undefined, name: string): string { if (!value || value.trim() === '') { throw new ConfigurationError( `${name} is required and must be provided either through environment variables or constructor options` ); } return value.trim(); } export function createConfig(options?: Partial<AzureDevOpsConfig>): AzureDevOpsConfig { const PAT = validateConfigValue( options?.pat ?? env.AZURE_DEVOPS_PAT, 'Personal Access Token (pat)' ); const ORG = validateConfigValue( options?.org ?? env.AZURE_DEVOPS_ORG, 'Organization (org)' ); const PROJECT = validateConfigValue( options?.project ?? env.AZURE_DEVOPS_PROJECT, 'Project (project)' ); if (!ORG.match(/^[a-zA-Z0-9-_]+$/)) { throw new ConfigurationError( 'Organization name must contain only alphanumeric characters, hyphens, and underscores' ); } return { pat: PAT, org: ORG, project: PROJECT, orgUrl: `https://dev.azure.com/${ORG}`, }; } ``` -------------------------------------------------------------------------------- /src/api/connection.ts: -------------------------------------------------------------------------------- ```typescript import * as azdev from 'azure-devops-node-api'; import { WebApi } from 'azure-devops-node-api'; import { AzureDevOpsConfig } from '../config/environment.js'; import { WikiApi } from './wiki.js'; export class AzureDevOpsConnection { private static instance: WebApi | null = null; private static config: AzureDevOpsConfig; private static wikiApi: WikiApi | null = null; public static initialize(config: AzureDevOpsConfig): void { this.config = config; // Reset instances when config changes this.instance = null; this.wikiApi = null; } public static getInstance(): WebApi { if (!this.config) { throw new Error('AzureDevOpsConnection must be initialized with config before use'); } if (!this.instance) { const authHandler = azdev.getPersonalAccessTokenHandler(this.config.pat); this.instance = new azdev.WebApi(this.config.orgUrl, authHandler); } return this.instance; } public static getWikiApi(): WikiApi { if (!this.config) { throw new Error('AzureDevOpsConnection must be initialized with config before use'); } if (!this.wikiApi) { const connection = this.getInstance(); this.wikiApi = new WikiApi(connection, this.config); } return this.wikiApi; } } ``` -------------------------------------------------------------------------------- /src/tools/wiki/create.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; import { WikiType } from 'azure-devops-node-api/interfaces/WikiInterfaces.js'; interface CreateWikiArgs { name: string; projectId?: string; mappedPath?: string; } export async function createWiki(args: CreateWikiArgs, config: AzureDevOpsConfig) { if (!args.name) { throw new McpError(ErrorCode.InvalidParams, 'Wiki name is required'); } AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const wikiApi = await connection.getWikiApi(); try { const wikiCreateParams = { name: args.name, projectId: args.projectId || config.project, mappedPath: args.mappedPath || '/', type: WikiType.ProjectWiki, }; const wiki = await wikiApi.createWiki(wikiCreateParams, config.project); return { content: [ { type: 'text', text: JSON.stringify(wiki, null, 2), }, ], }; } catch (error: unknown) { if (error instanceof McpError) throw error; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new McpError( ErrorCode.InternalError, `Failed to create wiki: ${errorMessage}` ); } } ``` -------------------------------------------------------------------------------- /src/tools/pipeline/index.ts: -------------------------------------------------------------------------------- ```typescript import { getPipelines } from './get.js'; import { triggerPipeline } from './trigger.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; const definitions = [ { name: 'list_pipelines', description: 'List all pipelines in the project', inputSchema: { type: 'object', properties: { folder: { type: 'string', description: 'Filter pipelines by folder path (optional)', }, name: { type: 'string', description: 'Filter pipelines by name (optional)', }, }, }, }, { name: 'trigger_pipeline', description: 'Trigger a pipeline run', inputSchema: { type: 'object', properties: { pipelineId: { type: 'number', description: 'Pipeline ID to trigger', }, branch: { type: 'string', description: 'Branch to run the pipeline on (optional, defaults to default branch)', }, variables: { type: 'object', description: 'Pipeline variables to override (optional)', additionalProperties: { type: 'string', }, }, }, required: ['pipelineId'], }, }, ]; export const pipelineTools = { initialize: (config: AzureDevOpsConfig) => ({ getPipelines: (args: any) => getPipelines(args, config), triggerPipeline: (args: any) => triggerPipeline(args, config), definitions, }), definitions, }; ``` -------------------------------------------------------------------------------- /src/tools/pull-request/create.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; import { GitPullRequest } from 'azure-devops-node-api/interfaces/GitInterfaces.js'; interface CreatePullRequestArgs { repositoryId: string; sourceRefName: string; targetRefName: string; title: string; description?: string; reviewers?: string[]; } export async function createPullRequest(args: CreatePullRequestArgs, config: AzureDevOpsConfig) { if (!args.repositoryId || !args.sourceRefName || !args.targetRefName || !args.title) { throw new McpError( ErrorCode.InvalidParams, 'Repository ID, source branch, target branch, and title are required' ); } AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const gitApi = await connection.getGitApi(); try { const pullRequestToCreate: GitPullRequest = { sourceRefName: args.sourceRefName, targetRefName: args.targetRefName, title: args.title, description: args.description, reviewers: args.reviewers?.map(id => ({ id })), }; const createdPr = await gitApi.createPullRequest( pullRequestToCreate, args.repositoryId, config.project ); return { content: [ { type: 'text', text: JSON.stringify(createdPr, null, 2), }, ], }; } catch (error: unknown) { if (error instanceof McpError) throw error; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new McpError( ErrorCode.InternalError, `Failed to create pull request: ${errorMessage}` ); } } ``` -------------------------------------------------------------------------------- /src/tools/pull-request/get.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; import { PullRequestStatus } from 'azure-devops-node-api/interfaces/GitInterfaces.js'; interface GetPullRequestsArgs { status?: 'active' | 'completed' | 'abandoned'; creatorId?: string; repositoryId?: string; } export async function getPullRequests(args: GetPullRequestsArgs, config: AzureDevOpsConfig) { AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const gitApi = await connection.getGitApi(); try { let statusFilter: PullRequestStatus | undefined; if (args.status) { switch (args.status) { case 'active': statusFilter = 1; // PullRequestStatus.Active break; case 'completed': statusFilter = 3; // PullRequestStatus.Completed break; case 'abandoned': statusFilter = 2; // PullRequestStatus.Abandoned break; } } const searchCriteria = { status: statusFilter, creatorId: args.creatorId, repositoryId: args.repositoryId, }; const pullRequests = await gitApi.getPullRequests( args.repositoryId || config.project, searchCriteria ); return { content: [ { type: 'text', text: JSON.stringify(pullRequests, null, 2), }, ], }; } catch (error: unknown) { if (error instanceof McpError) throw error; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new McpError( ErrorCode.InternalError, `Failed to get pull requests: ${errorMessage}` ); } } ``` -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- ```typescript export class BaseError extends Error { constructor(message: string) { super(message); this.name = this.constructor.name; // Restore prototype chain in Node.js Object.setPrototypeOf(this, BaseError.prototype); } } export class ConfigurationError extends BaseError { constructor(message: string) { super(message); Object.setPrototypeOf(this, ConfigurationError.prototype); } } export class ApiError extends BaseError { constructor( message: string, public readonly statusCode?: number, public readonly response?: unknown ) { super(message); Object.setPrototypeOf(this, ApiError.prototype); } } export class WikiError extends ApiError { constructor( message: string, statusCode?: number, public readonly wikiId?: string, public readonly path?: string, response?: unknown ) { super(message, statusCode, response); Object.setPrototypeOf(this, WikiError.prototype); } } export class WikiNotFoundError extends WikiError { constructor(wikiId: string) { super(`Wiki with ID ${wikiId} not found`, 404, wikiId); Object.setPrototypeOf(this, WikiNotFoundError.prototype); } } export class WikiPageNotFoundError extends WikiError { constructor(wikiId: string, path: string) { super(`Wiki page not found at path ${path}`, 404, wikiId, path); Object.setPrototypeOf(this, WikiPageNotFoundError.prototype); } } export class AuthenticationError extends ApiError { constructor(message: string = 'Authentication failed') { super(message, 401); Object.setPrototypeOf(this, AuthenticationError.prototype); } } export class NotFoundError extends ApiError { constructor(message: string) { super(message, 404); Object.setPrototypeOf(this, NotFoundError.prototype); } } ``` -------------------------------------------------------------------------------- /src/tools/pipeline/trigger.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; interface TriggerPipelineArgs { pipelineId: number; branch?: string; variables?: Record<string, string>; } export async function triggerPipeline(args: TriggerPipelineArgs, config: AzureDevOpsConfig) { if (!args.pipelineId) { throw new McpError(ErrorCode.InvalidParams, 'Pipeline ID is required'); } AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const pipelineApi = await connection.getBuildApi(); try { // Get pipeline definition first const definition = await pipelineApi.getDefinition( config.project, args.pipelineId ); if (!definition) { throw new McpError( ErrorCode.InvalidParams, `Pipeline with ID ${args.pipelineId} not found` ); } // Create build parameters const build = { definition: { id: args.pipelineId, }, project: definition.project, sourceBranch: args.branch || definition.repository?.defaultBranch || 'main', parameters: args.variables ? JSON.stringify(args.variables) : undefined, }; // Queue new build const queuedBuild = await pipelineApi.queueBuild(build, config.project); return { content: [ { type: 'text', text: JSON.stringify(queuedBuild, null, 2), }, ], }; } catch (error: unknown) { if (error instanceof McpError) throw error; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new McpError( ErrorCode.InternalError, `Failed to trigger pipeline: ${errorMessage}` ); } } ``` -------------------------------------------------------------------------------- /src/tools/wiki/update.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; interface UpdateWikiPageArgs { wikiIdentifier: string; path: string; content: string; comment?: string; } export async function updateWikiPage(args: UpdateWikiPageArgs, config: AzureDevOpsConfig) { if (!args.wikiIdentifier || !args.path || !args.content) { throw new McpError( ErrorCode.InvalidParams, 'Wiki identifier, page path, and content are required' ); } AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const wikiApi = await connection.getWikiApi(); try { const wiki = await wikiApi.getWiki(config.project, args.wikiIdentifier); if (!wiki || !wiki.id) { throw new McpError( ErrorCode.InvalidParams, `Wiki ${args.wikiIdentifier} not found` ); } const updateParams = { content: args.content, comment: args.comment || `Updated page ${args.path}`, }; // Da die Wiki-API keine direkte Methode zum Aktualisieren von Seiten bietet, // geben wir vorerst nur die Wiki-Informationen zurück return { content: [ { type: 'text', text: JSON.stringify({ wiki, path: args.path, message: 'Wiki page update is not supported in the current API version', requestedUpdate: updateParams }, null, 2), }, ], }; } catch (error: unknown) { if (error instanceof McpError) throw error; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new McpError( ErrorCode.InternalError, `Failed to update wiki page: ${errorMessage}` ); } } ``` -------------------------------------------------------------------------------- /src/tools/wiki/get.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; interface GetWikiPageArgs { wikiIdentifier: string; path: string; version?: string; includeContent?: boolean; } export async function getWikis(args: Record<string, never>, config: AzureDevOpsConfig) { AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const wikiApi = await connection.getWikiApi(); const wikis = await wikiApi.getAllWikis(config.project); return { content: [ { type: 'text', text: JSON.stringify(wikis, null, 2), }, ], }; } export async function getWikiPage(args: GetWikiPageArgs, config: AzureDevOpsConfig) { if (!args.wikiIdentifier || !args.path) { throw new McpError( ErrorCode.InvalidParams, 'Wiki identifier and page path are required' ); } AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const wikiApi = await connection.getWikiApi(); try { // Get wiki information const wiki = await wikiApi.getWiki(config.project, args.wikiIdentifier); if (!wiki || !wiki.id) { throw new McpError( ErrorCode.InvalidParams, `Wiki ${args.wikiIdentifier} not found` ); } // For now, we can only return the wiki information since the page API is not available return { content: [ { type: 'text', text: JSON.stringify({ id: wiki.id, name: wiki.name, path: args.path, message: 'Wiki page content retrieval is not supported in the current API version' }, null, 2), }, ], }; } catch (error: unknown) { if (error instanceof McpError) throw error; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new McpError( ErrorCode.InternalError, `Failed to get wiki page: ${errorMessage}` ); } } ``` -------------------------------------------------------------------------------- /src/tools/wiki/index.ts: -------------------------------------------------------------------------------- ```typescript import { getWikis } from './get.js'; import { getWikiPage } from './get.js'; import { createWiki } from './create.js'; import { updateWikiPage } from './update.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; const definitions = [ { name: 'get_wikis', description: 'List all wikis in the project', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_wiki_page', description: 'Get a wiki page by path', inputSchema: { type: 'object', properties: { wikiIdentifier: { type: 'string', description: 'Wiki identifier', }, path: { type: 'string', description: 'Page path', }, version: { type: 'string', description: 'Version (optional, defaults to main)', }, includeContent: { type: 'boolean', description: 'Include page content (optional, defaults to true)', }, }, required: ['wikiIdentifier', 'path'], }, }, { name: 'create_wiki', description: 'Create a new wiki', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Wiki name', }, projectId: { type: 'string', description: 'Project ID (optional, defaults to current project)', }, mappedPath: { type: 'string', description: 'Mapped path (optional, defaults to /)', }, }, required: ['name'], }, }, { name: 'update_wiki_page', description: 'Create or update a wiki page', inputSchema: { type: 'object', properties: { wikiIdentifier: { type: 'string', description: 'Wiki identifier', }, path: { type: 'string', description: 'Page path', }, content: { type: 'string', description: 'Page content in markdown format', }, comment: { type: 'string', description: 'Comment for the update (optional)', }, }, required: ['wikiIdentifier', 'path', 'content'], }, }, ]; export const wikiTools = { initialize: (config: AzureDevOpsConfig) => ({ getWikis: (args: Record<string, never>) => getWikis(args, config), getWikiPage: (args: { wikiIdentifier: string; path: string; version?: string; includeContent?: boolean }) => getWikiPage(args, config), createWiki: (args: { name: string; projectId?: string; mappedPath?: string }) => createWiki(args, config), updateWikiPage: (args: { wikiIdentifier: string; path: string; content: string; comment?: string }) => updateWikiPage(args, config), definitions, }), definitions, }; ``` -------------------------------------------------------------------------------- /src/tools/pull-request/index.ts: -------------------------------------------------------------------------------- ```typescript import { getPullRequests } from './get.js'; import { createPullRequest } from './create.js'; import { updatePullRequest } from './update.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; const definitions = [ { name: 'list_pull_requests', description: 'List all pull requests in the project', inputSchema: { type: 'object', properties: { status: { type: 'string', description: 'Filter by PR status (active, completed, abandoned)', enum: ['active', 'completed', 'abandoned'], }, creatorId: { type: 'string', description: 'Filter by creator ID (optional)', }, repositoryId: { type: 'string', description: 'Filter by repository ID (optional)', }, }, }, }, { name: 'create_pull_request', description: 'Create a new pull request', inputSchema: { type: 'object', properties: { repositoryId: { type: 'string', description: 'Repository ID', }, sourceRefName: { type: 'string', description: 'Source branch name (e.g. refs/heads/feature)', }, targetRefName: { type: 'string', description: 'Target branch name (e.g. refs/heads/main)', }, title: { type: 'string', description: 'Pull request title', }, description: { type: 'string', description: 'Pull request description', }, reviewers: { type: 'array', description: 'List of reviewer IDs (optional)', items: { type: 'string', }, }, }, required: ['repositoryId', 'sourceRefName', 'targetRefName', 'title'], }, }, { name: 'update_pull_request', description: 'Update an existing pull request', inputSchema: { type: 'object', properties: { pullRequestId: { type: 'number', description: 'Pull Request ID', }, status: { type: 'string', description: 'New status (active, abandoned, completed)', enum: ['active', 'abandoned', 'completed'], }, title: { type: 'string', description: 'New title (optional)', }, description: { type: 'string', description: 'New description (optional)', }, mergeStrategy: { type: 'string', description: 'Merge strategy (optional)', enum: ['squash', 'rebase', 'merge'], }, }, required: ['pullRequestId'], }, }, ]; export const pullRequestTools = { initialize: (config: AzureDevOpsConfig) => ({ getPullRequests: (args: any) => getPullRequests(args, config), createPullRequest: (args: any) => createPullRequest(args, config), updatePullRequest: (args: any) => updatePullRequest(args, config), definitions, }), definitions, }; ``` -------------------------------------------------------------------------------- /src/tools/pull-request/update.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { AzureDevOpsConnection } from '../../api/connection.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; import { GitPullRequest, PullRequestStatus, GitPullRequestMergeStrategy } from 'azure-devops-node-api/interfaces/GitInterfaces.js'; interface UpdatePullRequestArgs { pullRequestId: number; status?: 'active' | 'abandoned' | 'completed'; title?: string; description?: string; mergeStrategy?: 'squash' | 'rebase' | 'merge'; } export async function updatePullRequest(args: UpdatePullRequestArgs, config: AzureDevOpsConfig) { if (!args.pullRequestId) { throw new McpError(ErrorCode.InvalidParams, 'Pull Request ID is required'); } AzureDevOpsConnection.initialize(config); const connection = AzureDevOpsConnection.getInstance(); const gitApi = await connection.getGitApi(); try { // Get current PR const currentPr = await gitApi.getPullRequestById(args.pullRequestId, config.project); if (!currentPr) { throw new McpError( ErrorCode.InvalidParams, `Pull Request with ID ${args.pullRequestId} not found` ); } if (!currentPr.repository?.id) { throw new McpError( ErrorCode.InvalidParams, `Repository information not found for PR ${args.pullRequestId}` ); } // Prepare update const prUpdate: GitPullRequest = { ...currentPr, title: args.title || currentPr.title, description: args.description || currentPr.description, }; // Handle status changes if (args.status) { switch (args.status) { case 'active': prUpdate.status = 1 as PullRequestStatus; // Active break; case 'abandoned': prUpdate.status = 2 as PullRequestStatus; // Abandoned break; case 'completed': prUpdate.status = 3 as PullRequestStatus; // Completed if (args.mergeStrategy) { let mergeStrategyValue: GitPullRequestMergeStrategy; switch (args.mergeStrategy) { case 'squash': mergeStrategyValue = 3; // GitPullRequestMergeStrategy.Squash break; case 'rebase': mergeStrategyValue = 2; // GitPullRequestMergeStrategy.Rebase break; case 'merge': mergeStrategyValue = 1; // GitPullRequestMergeStrategy.NoFastForward break; default: mergeStrategyValue = 1; // Default to no-fast-forward } prUpdate.completionOptions = { mergeStrategy: mergeStrategyValue, deleteSourceBranch: true, squashMerge: args.mergeStrategy === 'squash', }; } break; } } // Update PR const updatedPr = await gitApi.updatePullRequest( prUpdate, currentPr.repository.id, args.pullRequestId, config.project ); return { content: [ { type: 'text', text: JSON.stringify(updatedPr, null, 2), }, ], }; } catch (error: unknown) { if (error instanceof McpError) throw error; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new McpError( ErrorCode.InternalError, `Failed to update pull request: ${errorMessage}` ); } } ``` -------------------------------------------------------------------------------- /src/tools/work-item/index.ts: -------------------------------------------------------------------------------- ```typescript import { getWorkItem } from './get.js'; import { listWorkItems } from './list.js'; import { createWorkItem } from './create.js'; import { updateWorkItem } from './update.js'; import { AzureDevOpsConfig } from '../../config/environment.js'; import type { WorkItem, WorkItemBatchGetRequest, Wiql } from 'azure-devops-node-api/interfaces/WorkItemTrackingInterfaces.js'; import type { JsonPatchOperation } from 'azure-devops-node-api/interfaces/common/VSSInterfaces.js'; const definitions = [ { name: 'get_work_item', description: 'Get work items by IDs', inputSchema: { type: 'object', properties: { ids: { type: 'array', items: { type: 'number' }, description: 'Work item IDs', }, fields: { type: 'array', items: { type: 'string' }, description: 'Fields to include (e.g., "System.Title", "System.State")', }, asOf: { type: 'string', format: 'date-time', description: 'As of a specific date (ISO 8601)', }, $expand: { type: 'number', enum: [0, 1, 2, 3, 4], description: 'Expand options (None=0, Relations=1, Fields=2, Links=3, All=4)', }, errorPolicy: { type: 'number', enum: [1, 2], description: 'Error policy (Fail=1, Omit=2)', } }, required: ['ids'], }, }, { name: 'list_work_items', description: 'List work items from a board', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'WIQL query to filter work items', }, }, required: ['query'], }, }, { name: 'create_work_item', description: 'Create a new work item using JSON patch operations', inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'Work item type (e.g., "Bug", "Task", "User Story")', }, document: { type: 'array', items: { type: 'object', properties: { op: { type: 'string', enum: ['add', 'remove', 'replace', 'move', 'copy', 'test'], description: 'The patch operation to perform', }, path: { type: 'string', description: 'The path for the operation (e.g., /fields/System.Title)', }, value: { description: 'The value for the operation', }, }, required: ['op', 'path'], }, description: 'Array of JSON patch operations to apply', }, }, required: ['type', 'document'], }, }, { name: 'update_work_item', description: 'Update an existing work item using JSON patch operations', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'ID of the work item to update', }, document: { type: 'array', items: { type: 'object', properties: { op: { type: 'string', enum: ['add', 'remove', 'replace', 'move', 'copy', 'test'], description: 'The patch operation to perform', }, path: { type: 'string', description: 'The path for the operation (e.g., /fields/System.Title)', }, value: { description: 'The value for the operation', }, }, required: ['op', 'path'], }, description: 'Array of JSON patch operations to apply', }, }, required: ['id', 'document'], }, }, ]; export const workItemTools = { initialize: (config: AzureDevOpsConfig) => ({ getWorkItem: (args: WorkItemBatchGetRequest) => getWorkItem(args, config), listWorkItems: (args: Wiql) => listWorkItems(args, config), createWorkItem: (args: { type: string; document: JsonPatchOperation[] }) => createWorkItem(args, config), updateWorkItem: (args: { id: number; document: JsonPatchOperation[] }) => updateWorkItem(args, config), definitions, }), definitions, }; ``` -------------------------------------------------------------------------------- /src/api/wiki.ts: -------------------------------------------------------------------------------- ```typescript import { WebApi } from 'azure-devops-node-api'; import { AzureDevOpsConfig } from '../config/environment.js'; import { WikiError, WikiNotFoundError, WikiPageNotFoundError } from '../errors.js'; import fetch from 'node-fetch'; import type { Wiki, WikiPage, WikiPageResponse, WikiType, WikiCreateParameters, WikiPageCreateOrUpdateParameters } from 'azure-devops-node-api/interfaces/WikiInterfaces.js'; interface WikiListResponse { count: number; value: Wiki[]; } interface WikiCreateResponse extends WikiCreateParameters { id: string; createdBy: { id: string; displayName: string; uniqueName: string; }; createdDate: string; } interface WikiPageUpdateResponse extends WikiPageResponse { lastUpdatedBy: { id: string; displayName: string; uniqueName: string; }; lastUpdatedDate: string; } export class WikiApi { private connection: WebApi; private baseUrl: string; private config: AzureDevOpsConfig; constructor(connection: WebApi, config: AzureDevOpsConfig) { this.connection = connection; this.config = config; this.baseUrl = `${config.orgUrl}/${config.project}/_apis/wiki`; } private async getAuthHeader(): Promise<string> { const token = Buffer.from(`:${this.config.pat}`).toString('base64'); return `Basic ${token}`; } async createWiki(name: string, projectId?: string, mappedPath?: string): Promise<WikiCreateResponse> { const authHeader = await this.getAuthHeader(); const response = await fetch(`${this.baseUrl}?api-version=7.0`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify({ name, projectId: projectId || this.config.project, type: 'projectWiki', mappedPath: mappedPath || '/', }), }); if (!response.ok) { throw new WikiError( `Failed to create wiki: ${response.statusText}`, response.status, undefined, undefined, await response.text() ); } return response.json(); } async getAllWikis(): Promise<WikiListResponse> { const authHeader = await this.getAuthHeader(); const response = await fetch(`${this.baseUrl}?api-version=7.0`, { headers: { Authorization: authHeader, }, }); if (!response.ok) { throw new WikiError( `Failed to get wikis: ${response.statusText}`, response.status, undefined, undefined, await response.text() ); } return response.json(); } async getWikiPage(wikiIdentifier: string, path: string): Promise<WikiPage> { const authHeader = await this.getAuthHeader(); const encodedPath = encodeURIComponent(path); const response = await fetch( `${this.baseUrl}/${wikiIdentifier}/pages?path=${encodedPath}&api-version=7.0`, { headers: { Authorization: authHeader, }, } ); if (response.status === 404) { if (response.statusText.includes('Wiki not found')) { throw new WikiNotFoundError(wikiIdentifier); } throw new WikiPageNotFoundError(wikiIdentifier, path); } if (!response.ok) { throw new WikiError( `Failed to get wiki page: ${response.statusText}`, response.status, wikiIdentifier, path, await response.text() ); } return response.json(); } async updateWikiPage( wikiIdentifier: string, path: string, content: string, comment?: string ): Promise<WikiPageUpdateResponse> { const authHeader = await this.getAuthHeader(); const encodedPath = encodeURIComponent(path); const response = await fetch( `${this.baseUrl}/${wikiIdentifier}/pages?path=${encodedPath}&api-version=7.0`, { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: authHeader, }, body: JSON.stringify({ content, comment: comment || `Updated page ${path}`, }), } ); if (response.status === 404) { if (response.statusText.includes('Wiki not found')) { throw new WikiNotFoundError(wikiIdentifier); } throw new WikiPageNotFoundError(wikiIdentifier, path); } if (!response.ok) { throw new WikiError( `Failed to update wiki page: ${response.statusText}`, response.status, wikiIdentifier, path, await response.text() ); } return response.json(); } } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, JSONRPCResponseSchema, JSONRPCResponse } from '@modelcontextprotocol/sdk/types.js'; // Import all tools import { workItemTools } from './tools/work-item/index.js'; import { boardTools } from './tools/board/index.js'; import { wikiTools } from './tools/wiki/index.js'; import { projectTools } from './tools/project/index.js'; import { pipelineTools } from './tools/pipeline/index.js'; import { pullRequestTools } from './tools/pull-request/index.js'; import { AzureDevOpsConfig, createConfig } from './config/environment.js'; import { ListToolsResult } from '@modelcontextprotocol/sdk/types.js'; // TODO: Use the proper ToolDefinition type from MCP SDK // type ToolDefinition = ReturnType<typeof workItemTools.initialize>['definitions'][number]; type ToolDefinition = any; // TODO: Use a proper ToolInstace definition from MCP SDK //interface ToolInstances { // workItem: ReturnType<typeof workItemTools.initialize>; // board: ReturnType<typeof boardTools.initialize>; // wiki: ReturnType<typeof wikiTools.initialize>; // project: ReturnType<typeof projectTools.initialize>; // pipeline: ReturnType<typeof pipelineTools.initialize>; // pullRequest: ReturnType<typeof pullRequestTools.initialize>; //} type ToolInstances = any; // Type Validations function validateArgs<T>(args: Record<string, unknown> | undefined, errorMessage: string): T { if (!args) { throw new McpError(ErrorCode.InvalidParams, errorMessage); } return args as T; } type MCPResponse = JSONRPCResponse["result"] // Response Formatting function formatResponse(data: unknown): MCPResponse { if (data && typeof data === 'object' && 'content' in data) { return data as MCPResponse; } return { content: [ { type: 'text', text: JSON.stringify(data, null, 2), }, ], }; } class AzureDevOpsServer { private server: Server; private config: AzureDevOpsConfig; private toolDefinitions: ToolDefinition[]; constructor(options?: Partial<Omit<AzureDevOpsConfig, 'orgUrl'>>) { this.config = createConfig(options); // Initialize tools with config const toolInstances = { workItem: workItemTools.initialize(this.config), board: boardTools.initialize(this.config), wiki: wikiTools.initialize(this.config), project: projectTools.initialize(this.config), pipeline: pipelineTools.initialize(this.config), pullRequest: pullRequestTools.initialize(this.config), }; // Combine all tool definitions this.toolDefinitions = [ ...toolInstances.workItem.definitions, ...toolInstances.board.definitions, ...toolInstances.wiki.definitions, ...toolInstances.project.definitions, ...toolInstances.pipeline.definitions, ...toolInstances.pullRequest.definitions, ]; this.server = new Server( { name: 'azure-devops-server', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(toolInstances); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupToolHandlers(tools: ToolInstances) { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: this.toolDefinitions, })); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { try { let result; switch (request.params.name) { // Work Item Tools case 'get_work_item': result = await tools.workItem.getWorkItem(request.params.arguments); break; case 'list_work_items': result = await tools.workItem.listWorkItems(request.params.arguments); break; // Board Tools case 'get_boards': result = await tools.board.getBoards(request.params.arguments); break; // Wiki Tools case 'get_wikis': result = await tools.wiki.getWikis(request.params.arguments); break; case 'get_wiki_page': result = await tools.wiki.getWikiPage(request.params.arguments); break; case 'create_wiki': result = await tools.wiki.createWiki(request.params.arguments); break; case 'update_wiki_page': result = await tools.wiki.updateWikiPage(request.params.arguments); break; // Project Tools case 'list_projects': result = await tools.project.listProjects(request.params.arguments); break; // Pipeline Tools case 'list_pipelines': result = await tools.pipeline.getPipelines( validateArgs(request.params.arguments, 'Pipeline arguments required') ); break; case 'trigger_pipeline': result = await tools.pipeline.triggerPipeline( validateArgs(request.params.arguments, 'Pipeline trigger arguments required') ); break; // Pull Request Tools case 'list_pull_requests': result = await tools.pullRequest.getPullRequests( validateArgs(request.params.arguments, 'Pull request list arguments required') ); break; case 'get_pull_request': result = await tools.pullRequest.getPullRequest( validateArgs(request.params.arguments, 'Pull request ID required') ); break; case 'create_pull_request': result = await tools.pullRequest.createPullRequest( validateArgs(request.params.arguments, 'Pull request creation arguments required') ); break; case 'update_pull_request': result = await tools.pullRequest.updatePullRequest( validateArgs(request.params.arguments, 'Pull request update arguments required') ); break; default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } // Ensure consistent response format const response = formatResponse(result); return { _meta: request.params._meta, ...response }; } catch (error: unknown) { if (error instanceof McpError) throw error; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new McpError( ErrorCode.InternalError, `Azure DevOps API error: ${errorMessage}` ); } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Azure DevOps MCP server running on stdio'); } } // Allow configuration through constructor or environment variables const server = new AzureDevOpsServer(); server.run().catch(console.error); ```