Metadata-Version: 2.4
Name: devops-mcp
Version: 1.2.1
Summary: An MCP server for interacting with Azure DevOps
Author: Ryan James
License-Expression: MIT
Project-URL: Homepage, https://github.com/ryanmichaeljames/devops-mcp
Project-URL: Repository, https://github.com/ryanmichaeljames/devops-mcp
Project-URL: Issues, https://github.com/ryanmichaeljames/devops-mcp/issues
Project-URL: Changelog, https://github.com/ryanmichaeljames/devops-mcp/blob/main/CHANGELOG.md
Keywords: mcp,model-context-protocol,azure-devops,devops,copilot,llm
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: azure-identity>=1.16.0
Requires-Dist: httpx<1.0,>=0.20.0
Requires-Dist: mcp[cli]>=1.27.0
Requires-Dist: pydantic>=2.0.0
Dynamic: license-file

# devops-mcp

[![PyPI](https://img.shields.io/pypi/v/devops-mcp)](https://pypi.org/project/devops-mcp/)
[![Python](https://img.shields.io/pypi/pyversions/devops-mcp)](https://pypi.org/project/devops-mcp/)
[![License: MIT](https://img.shields.io/github/license/ryanmichaeljames/devops-mcp)](LICENSE)

An [MCP](https://modelcontextprotocol.io/) server that exposes Azure DevOps as tools for LLMs — pipelines, repositories, pull requests, and work items. Built with [FastMCP](https://github.com/modelcontextprotocol/python-sdk) over stdio transport.

Communicates over **stdio** and works with GitHub Copilot, Claude Code, and any MCP-compatible client.

---

## Quick Start

**1. Install dependencies**

```bash
uv sync
```

**2. Configure** — add to your MCP client config (see [MCP Client Setup](#mcp-client-setup) below).

**3. Run the server**

```bash
uv run devops-mcp
```

---

## Installation

### Prerequisites

- Python `>=3.10`
- [uv](https://docs.astral.sh/uv/) (recommended)
- A Microsoft Entra ID identity with access to Azure DevOps

### Install dependencies

```bash
uv sync
```

### Build the package

```bash
uv run python -m build
```

---

## Configuration

All configuration is driven by environment variables — no secrets in code, no hardcoded org names or tenant IDs.

| Variable | Required? | Default | Description |
|---|---|---|---|
| `AZDO_AUTH_TYPE` | No | `default` | Authentication method. One of: `default`, `azure_cli`, `interactive`, `client_secret`, `managed_identity`. `default` tries all credential sources in order (environment variables, Azure CLI, managed identity) and is the right choice for local development. See [Authentication](#authentication). |
| `AZDO_ORGANIZATION` | No | — | Default Azure DevOps organization name. Can be overridden per tool call. Required if not supplied per call. |
| `AZDO_PROJECT` | No | — | Default Azure DevOps project name. Can be overridden per tool call. Required if not supplied per call. |
| `AZDO_TENANT_ID` | Conditional | — | Microsoft Entra ID tenant ID. Required for `client_secret`. Recommended for `interactive` to constrain sign-in to the correct tenant. |
| `AZDO_CLIENT_ID` | `client_secret` only | — | Service principal client ID. |
| `AZDO_CLIENT_SECRET` | `client_secret` only | — | Service principal client secret. |
| `AZDO_LOG_LEVEL` | No | `INFO` | Log verbosity: `DEBUG`, `INFO`, `WARNING`, `ERROR`. All logs go to stderr; stdout is reserved for MCP stdio transport. |
| `AZDO_ALLOW_WRITE` | No | off | Set `true` to register create, update, tag, link, and comment (write) tools. When unset the server is read-only — write tools are not visible to the agent at all. |
| `AZDO_ALLOW_DELETE` | No | off | Set `true` to register delete tools. When unset, delete tools are not visible to the agent. |
| `AZDO_EPHEMERAL_TOKEN` | No | `false` | **Interactive auth only.** When `false` (the default), the MSAL token cache is persisted to disk via the OS secret store (Windows DPAPI, macOS Keychain, Linux libsecret), and an `AuthenticationRecord` sidecar is written to `~/.devops-mcp/auth-record.json` so subsequent server restarts authenticate silently without a new browser prompt. Set `true`, `1`, or `yes` to use an in-memory-only cache (no disk cache, no sidecar) — re-prompts on every restart. Invalid values fall back to `false` with a logged warning. Has no effect on any auth type other than `interactive`. |
| `AZDO_TOKEN_CACHE_PROFILE` | No | — | **Interactive auth only.** A filename-safe suffix (`[A-Za-z0-9_-]`) appended to the MSAL cache name and the `AuthenticationRecord` sidecar so two server instances signed in to **different tenants/accounts** on the same host keep separate caches instead of overwriting each other's pinned account. Omit (or leave empty) for a single-tenant setup — the original shared filenames are used. Characters outside `[A-Za-z0-9_-]` raise an error rather than being silently dropped (sanitizing could collapse two distinct profiles into one shared cache). |
| `AZDO_AUTH_TIMEOUT_SECONDS` | No | `30` | Maximum seconds to wait for credential acquisition before failing with an auth error. Applies to all auth types. Invalid or non-positive values fall back to `30`. Increase this in slow-network or MFA-heavy environments. |

---

## Authentication

The server uses **Microsoft Entra ID (Azure AD) OAuth 2.0** via the [`azure-identity`](https://pypi.org/project/azure-identity/) library. Set `AZDO_AUTH_TYPE` to select a method.

| `AZDO_AUTH_TYPE` | Description | Best for |
|---|---|---|
| `default` *(default)* | `DefaultAzureCredential` — tries environment variables, Azure CLI session, managed identity, and other sources in order. Does not prompt in-process. | **Recommended — works everywhere** |
| `azure_cli` | Uses the active Azure CLI session (`az login`). Does not prompt in-process. | Local development with an existing CLI session |
| `interactive` | Opens a browser for interactive sign-in. Supports MFA and multi-account use. Benefits from the persistent token cache (on by default; disable with `AZDO_EPHEMERAL_TOKEN=true`): the first launch prompts; subsequent restarts reuse the cached refresh token silently while it remains valid. | Local development without a CLI session |
| `client_secret` | Service principal with client secret. Requires `AZDO_TENANT_ID`, `AZDO_CLIENT_ID`, and `AZDO_CLIENT_SECRET`. | CI/CD, unattended automation |
| `managed_identity` | Azure Managed Identity. No credentials to manage. | Azure-hosted workloads (VMs, Functions, Container Apps) |

**For `default` / local dev:** run `az login` once — `DefaultAzureCredential` will pick it up automatically.

**For `interactive`:** a browser window opens on first use. Set `AZDO_TENANT_ID` to constrain sign-in to a specific Entra ID tenant (recommended when multiple accounts are in use). The persistent token cache is on by default, so subsequent restarts are silent; set `AZDO_EPHEMERAL_TOKEN=true` to opt out.

**For `client_secret`:** also set `AZDO_TENANT_ID`, `AZDO_CLIENT_ID`, and `AZDO_CLIENT_SECRET`.

---

## Security

### Safe-by-default write and delete gates

Write and delete tools are **not registered by default** — they do not appear to the agent at all until explicitly enabled. The server is read-only until `AZDO_ALLOW_WRITE=true` and/or `AZDO_ALLOW_DELETE=true` are set. Each flag is independent; set only the ones you need.

### Env-driven configuration

All configuration is supplied via environment variables. No secrets, org names, project names, or tenant IDs are hardcoded. `.vscode/mcp.json` is gitignored because it may contain credentials.

### Stdout reserved for MCP transport

Stdout is exclusively reserved for MCP stdio transport messages. All server logs (including auth events) go to stderr via the Python `logging` module. Never redirect stdout to a log file.

### Token cache caveats (`interactive` auth)

By default the MSAL token cache is encrypted at rest using the OS secret store (Windows DPAPI, macOS Keychain, Linux libsecret). The `AuthenticationRecord` sidecar stored at `~/.devops-mcp/auth-record.json` contains only account metadata (home account ID, tenant, authority, username) — no tokens or client secrets.

On headless Linux without a secret store (e.g., no GNOME Keyring / libsecret installed), the OS-encrypted cache may be unavailable. The server logs an actionable warning and falls back to an in-memory-only cache. Set `AZDO_EPHEMERAL_TOKEN=true` to suppress the warning and always use in-memory cache on such hosts.

### Multiple tenants/accounts on one host (`interactive` auth)

The default cache and sidecar filenames (`devops-mcp.cache`, `~/.devops-mcp/auth-record.json`) are shared per host, so two `interactive` sessions signed in to **different tenants/accounts** would overwrite each other's pinned account. Give each session a distinct `AZDO_TOKEN_CACHE_PROFILE` (e.g. `prod`, `dev`) to keep their caches and `AuthenticationRecord` sidecars separate. The profile is a tenant-wide cache key: each entry signs in once (its own browser prompt) and then restarts silently as its own account, while tools still receive the specific `organization`/`project` per call. The profiles never collide.

Register two server entries, each with its own profile and (recommended) matching `AZDO_TENANT_ID`:

```json
{
  "servers": {
    "devops-mcp-prod": {
      "type": "stdio",
      "command": "uv",
      "args": ["run", "devops-mcp"],
      "env": {
        "AZDO_AUTH_TYPE": "interactive",
        "AZDO_TENANT_ID": "<prod-tenant-id>",
        "AZDO_TOKEN_CACHE_PROFILE": "prod"
      }
    },
    "devops-mcp-dev": {
      "type": "stdio",
      "command": "uv",
      "args": ["run", "devops-mcp"],
      "env": {
        "AZDO_AUTH_TYPE": "interactive",
        "AZDO_TENANT_ID": "<dev-tenant-id>",
        "AZDO_TOKEN_CACHE_PROFILE": "dev"
      }
    }
  }
}
```

### Resilience behavior

These behaviors are built in and require no configuration:

- **Automatic retries** — requests that receive `429` (throttling) or transient gateway errors (`502`, `503`, `504`) are retried automatically with back-off and `Retry-After` header honoring. **Non-idempotent writes (POST, PATCH) are not retried on `5xx`** — a gateway error on a write may arrive after the server has already committed the operation; only `429` (which guarantees the request was rejected before processing) is safe to retry on all methods.
- **Response size cap** — responses larger than **5 MB** are replaced with an error asking the agent to narrow the query. For large pipeline logs, prefer `devops_get_run_timeline` to triage without pulling log text at all; `devops_get_run_log_content` defaults to a bounded 500-line page (`max_lines`, `tail`, `start_line`/`end_line`) with a paging envelope, and `devops_search_run_log` returns only matching lines.
- **Auth timeout** — credential acquisition is bounded by `AZDO_AUTH_TIMEOUT_SECONDS` (default 30 s). A slow or hung auth call releases the per-scope lock so subsequent callers are not serialized indefinitely.

---

## MCP Client Setup

### GitHub Copilot (VS Code)

Add to `.vscode/mcp.json` in your project root. Note: `.vscode/mcp.json` is gitignored because it may contain secrets.

**Default / local dev (recommended):**

```json
{
  "servers": {
    "devops-mcp": {
      "type": "stdio",
      "command": "uv",
      "args": ["run", "devops-mcp"],
      "env": {
        "AZDO_ORGANIZATION": "<your-org>",
        "AZDO_PROJECT": "<your-project>"
      }
    }
  }
}
```

**With write tools enabled:**

```json
{
  "servers": {
    "devops-mcp": {
      "type": "stdio",
      "command": "uv",
      "args": ["run", "devops-mcp"],
      "env": {
        "AZDO_ORGANIZATION": "<your-org>",
        "AZDO_PROJECT": "<your-project>",
        "AZDO_ALLOW_WRITE": "true"
      }
    }
  }
}
```

**Service principal (CI/CD):**

```json
{
  "servers": {
    "devops-mcp": {
      "type": "stdio",
      "command": "uv",
      "args": ["run", "devops-mcp"],
      "env": {
        "AZDO_AUTH_TYPE": "client_secret",
        "AZDO_TENANT_ID": "<your-tenant-id>",
        "AZDO_CLIENT_ID": "<your-client-id>",
        "AZDO_CLIENT_SECRET": "<your-client-secret>",
        "AZDO_ORGANIZATION": "<your-org>",
        "AZDO_PROJECT": "<your-project>"
      }
    }
  }
}
```

---

## Tools

**48 tools** across 6 domains. Tools marked with a gate are only registered when the corresponding env flag is set.

| Gate | Meaning |
|---|---|
| `default` | Always registered (reads and safe queries). |
| `write` | Registered only when `AZDO_ALLOW_WRITE=true`. |
| `delete` | Registered only when `AZDO_ALLOW_DELETE=true`. |

### Pipelines (10 tools)

| Tool | Gate | Description |
|---|---|---|
| `devops_list_pipelines` | default | List pipelines defined in a project |
| `devops_list_pipeline_runs` | default | List runs for a specific pipeline |
| `devops_get_pipeline_run` | default | Get details of a specific pipeline run |
| `devops_get_build` | default | Get build details by `buildId` (resolves a build URL to pipeline info) |
| `devops_list_run_logs` | default | List log metadata for a build by `buildId` |
| `devops_get_run_timeline` | default | Compact, filtered build timeline with inline failure messages — the recommended first stop for "why did this build fail," often needing zero log fetches |
| `devops_get_run_log_content` | default | Get plain-text content of a specific log; bounded to `max_lines` (default 500) by default, with `start_line`/`end_line`/`tail` windowing and a paging envelope (`has_more`/`next_start_line`) |
| `devops_search_run_log` | default | Search (grep) a log in-process and return only matching lines plus context — non-matching lines never reach the model |
| `devops_list_build_artifacts` | default | List artifacts produced by a build |
| `devops_run_pipeline` | write | Trigger a new pipeline run; optionally override branch, template parameters, or queue-time variables |

### Repositories (7 tools)

| Tool | Gate | Description |
|---|---|---|
| `devops_list_repositories` | default | List Git repositories in a project |
| `devops_get_repository` | default | Get details of a specific repository |
| `devops_list_branches` | default | List branches in a repository |
| `devops_get_file_content` | default | Get the text content of a file; supports optional `branch` or `commit_id`; binary files return an error |
| `devops_list_repository_items` | default | Browse files and folders; control depth with `recursion_level` (`oneLevel`, `full`, etc.) |
| `devops_list_commits` | default | List commits with optional filters for branch, author, and date range |
| `devops_get_commit` | default | Get details of a specific commit; set `change_count` to include changed file paths |

### Pull Requests (17 tools)

| Tool | Gate | Description |
|---|---|---|
| `devops_get_pull_request` | default | Get details of a specific pull request |
| `devops_list_pull_requests` | default | List pull requests with optional filters (status, branch, creator, reviewer, labels) |
| `devops_create_pull_request` | write | Create a new pull request, optionally linking work items |
| `devops_update_pull_request` | write | Update title, description, status, draft state, target branch, auto-complete, or completion options |
| `devops_complete_pull_request` | write | Complete (merge) a pull request — irreversible; confirm merge strategy and source-branch deletion first to avoid unwanted merge type or history loss |
| `devops_abandon_pull_request` | write | Abandon a pull request without merging |
| `devops_vote_pull_request` | write | Cast a reviewer vote (10 approve, 5 approve with suggestions, 0 reset, -5 waiting, -10 reject) |
| `devops_tag_pull_request` | write | Add labels/tags to a pull request |
| `devops_link_work_items_to_pull_request` | write | Link Azure Boards work items to a pull request |
| `devops_list_pull_request_threads` | default | List comment threads on a pull request |
| `devops_get_pull_request_thread` | default | Get a single comment thread with its comments |
| `devops_create_pull_request_thread` | write | Start a comment thread — general, or inline on a file/line via thread context |
| `devops_set_pull_request_thread_status` | write | Set a thread's status (`active`, `fixed`, `wontFix`, `closed`, `byDesign`, `pending`) |
| `devops_add_pull_request_comment` | write | Reply to an existing comment thread |
| `devops_update_pull_request_comment` | write | Edit the text of an existing comment |
| `devops_list_pull_request_iterations` | default | List a pull request's iterations (push history) |
| `devops_get_pull_request_changes` | default | List changed files for a PR iteration (path + change type) |

### Work Items (9 tools)

| Tool | Gate | Description |
|---|---|---|
| `devops_get_work_item` | default | Get a single work item by ID |
| `devops_list_work_items` | default | Bulk-fetch up to 200 work items by ID |
| `devops_query_work_items` | default | Query work items with WIQL, auto-fetching full details |
| `devops_create_work_item` | write | Create a new work item (large-text fields saved as markdown by default; `format=html` to opt out) |
| `devops_update_work_item` | write | Update fields on an existing work item (large-text fields saved as markdown by default; `format=html` to opt out) |
| `devops_add_work_item_comment` | write | Add a comment to a work item (markdown by default; `format=html` to opt out) |
| `devops_update_work_item_comment` | write | Update an existing work item comment (markdown by default; `format=html` to opt out) |
| `devops_list_work_item_types` | default | List work item types (e.g., Bug, Task, Epic) and their reference names |
| `devops_list_work_item_fields` | default | List field definitions for a work item type or all fields in the process |

### Discovery (2 tools)

| Tool | Gate | Description |
|---|---|---|
| `devops_list_projects` | default | List projects in an organization; use when project name is unknown |
| `devops_list_teams` | default | List teams in a project; supports `mine=true` to filter to the authenticated user's teams |

### Advanced Security (3 tools)

Requires GitHub Advanced Security for Azure DevOps (GHAzDo) to be enabled on the repository.

| Tool | Gate | Description |
|---|---|---|
| `devops_list_advanced_security_alerts` | default | List security alerts for a repository; filter by `alert_type` (`secret`, `dependency`, `code`), state, severity, rule, tool, or branch |
| `devops_get_advanced_security_alert` | default | Get a single alert by ID. `expand=validationFingerprint` can return secret values in cleartext — leave unset unless needed |
| `devops_update_advanced_security_alert` | write | Dismiss, re-activate, or mark an alert fixed; dismissing requires a dismissal reason |

---

## API Reference

All tools use the [Azure DevOps REST API](https://learn.microsoft.com/en-us/rest/api/azure/devops/). Pipeline, repository, and discovery tools use **v7.1**. Work item schema tools (`devops_list_work_item_types`, `devops_list_work_item_fields`) use **v7.1**. PR tools (get/list/create/update/tag/link) and work item write operations use **v7.2-preview**. Advanced Security alert tools use **v7.2-preview.1** on the `advsec.dev.azure.com` host.

**Note:** `run_id` and `build_id` share the same numeric value — a Pipelines API `run_id` is identical to the Build API `buildId` for the same run. This enables cross-API calls (e.g., use `devops_list_run_logs` to get log IDs, then `devops_get_run_log_content` with the same `build_id`).

### Efficient log retrieval

Pipeline logs can be huge and expensive to read. Work from cheapest to most expensive:

1. **`devops_get_run_timeline`** — start here for "why did it fail." Returns a compact, failure-filtered timeline with inline error messages, often answering the question with **zero log fetches**.
2. **`devops_search_run_log`** — grep a specific log in-process; only matching lines (plus context) are returned, so non-matching content never reaches the model.
3. **`devops_get_run_log_content`** — read log text directly. Bounded to `max_lines` (default 500) with `start_line`/`end_line`/`tail` windowing and a paging envelope (`has_more`, `next_start_line`) to page through large logs or grab just the `tail`.
