Skip to main content
Glama
README.md
<div align="center">

# πŸ›‘οΈ ADO Guard MCP

**A safety-first [Model Context Protocol](https://modelcontextprotocol.io) server for Azure DevOps.**
Let Claude, VS Code Copilot or Cursor work with your boards, pull requests and pipelines without handing them the keys.

[![CI](https://github.com/abdulbasit7010/ado-guard-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/abdulbasit7010/ado-guard-mcp/actions/workflows/ci.yml)
![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6)
![MCP](https://img.shields.io/badge/MCP-stdio-8b5cf6)
![License](https://img.shields.io/badge/license-MIT-green)

### [β–Ά Try the live playground](https://abdulbasit7010.github.io/ado-guard-mcp/) Β· no install, runs the real engine in your browser

<a href="https://abdulbasit7010.github.io/ado-guard-mcp/"><img src="docs/playground.png" alt="ADO Guard playground: policy panel, agent session with an approval prompt, tool list and audit log" width="900"></a>

</div>

---

## Why

Giving an LLM a Personal Access Token is all or nothing: if the agent can comment on a work item, it can also delete one,
and anything sensitive in a ticket goes straight into the model's context. **ADO Guard puts a policy engine between the model and Azure DevOps**:

| Guardrail | What it does |
| --- | --- |
| πŸ”’ **Read-only by default** | Zero config = no write tools exist. Disabled tools are never registered, so the model can't even see them. |
| πŸ‘οΈ **Dry run** | Write tools return a `before β†’ after` diff instead of changing anything. |
| βœ… **Human approval** | Risky calls return a preview and a **one-time token bound to the exact arguments**. The agent must show the preview and retry with the token after the user approves. Tokens are single-use and expire after 5 minutes. |
| 🎯 **Scoping** | Project allowlist, protected fields (e.g. `AreaPath`) and protected branches (`main`, `release/*`) for pipeline runs. |
| πŸ”‘ **Secret redaction** | PATs, bearer tokens, cloud keys, private keys and `password=` style secrets are stripped from every response. Emails can be masked too. |
| 🚦 **Rate limiting** | Sliding-window cap on writes per minute stops a looping agent. |
| πŸ“œ **Audit log** | Every call (allowed, denied, dry-run, pending or failed) is written to a JSONL file and exposed via a tool. |

## How it works

```
Claude / VS Code / Cursor ──MCP (stdio)──▢ ado-guard-mcp
                                              β”‚
                                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                   β”‚     GuardEngine      │◀── policy.json + env
                                   β”‚ expose β†’ validate β†’  β”‚
                                   β”‚ scope β†’ cap β†’ preview│──▢ audit.jsonl
                                   β”‚ β†’ approve β†’ throttle β”‚
                                   β”‚ β†’ execute β†’ redact   β”‚
                                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                              β”‚ PAT (never shown to the model)
                                   Azure DevOps REST API 7.1
```

The engine (`src/core`) is plain TypeScript with **no Node APIs**. The MCP server is a thin adapter around it, and the
[playground](https://abdulbasit7010.github.io/ado-guard-mcp/) bundles the same code for the browser with a mock organization.

## Quickstart

Runs locally over stdio. There's nothing to host.

**Try it with demo data** (no Azure DevOps account needed):

```bash
npx @modelcontextprotocol/inspector -e ADO_GUARD_MOCK=true -e ADO_GUARD_MODE=read-write \
  npx -y github:abdulbasit7010/ado-guard-mcp
```

**Claude Desktop** (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "azure-devops": {
      "command": "npx",
      "args": ["-y", "github:abdulbasit7010/ado-guard-mcp"],
      "env": {
        "ADO_ORG_URL": "https://dev.azure.com/your-org",
        "ADO_PAT": "<personal access token>",
        "ADO_GUARD_MODE": "read-write",
        "ADO_GUARD_PROJECTS": "Atlas,Helix",
        "ADO_GUARD_AUDIT_FILE": "/Users/you/.ado-guard/audit.jsonl"
      }
    }
  }
}
```

**VS Code** (`.vscode/mcp.json`):

```json
{
  "inputs": [{ "id": "ado_pat", "type": "promptString", "description": "Azure DevOps PAT", "password": true }],
  "servers": {
    "azure-devops": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "github:abdulbasit7010/ado-guard-mcp"],
      "env": {
        "ADO_ORG_URL": "https://dev.azure.com/your-org",
        "ADO_PAT": "${input:ado_pat}",
        "ADO_GUARD_POLICY": "${workspaceFolder}/ado-guard.policy.json"
      }
    }
  }
}
```

## Configuration

| Variable | Default | Description |
| --- | --- | --- |
| `ADO_ORG_URL` | (required) | e.g. `https://dev.azure.com/your-org` |
| `ADO_PAT` | (required) | Personal Access Token. Give it the narrowest scopes you need. |
| `ADO_GUARD_MOCK` | `false` | Use the built-in fictional org instead of a real one |
| `ADO_GUARD_POLICY` | | Path to a JSON policy file ([example](policy.example.json)) |
| `ADO_GUARD_MODE` | `read-only` | `read-only` or `read-write` |
| `ADO_GUARD_DRY_RUN` | `false` | Preview writes without executing |
| `ADO_GUARD_ALLOW_DESTRUCTIVE` | `false` | Enable `delete_work_item` and `run_pipeline` |
| `ADO_GUARD_PROJECTS` | all | Comma-separated project allowlist |
| `ADO_GUARD_AUDIT_FILE` | in-memory | Append audit entries as JSON lines |

Environment variables override the policy file. Everything else (`requireApproval`, `protectedFields`,
`protectedBranches`, `maxResults`, `rateLimit`, `redact`, `tools.allow/deny`) is set in the policy file, and every
field has a safe default. See [`src/core/policy.ts`](src/core/policy.ts).

## Tools

| Tool | Risk | Notes |
| --- | --- | --- |
| `list_projects` | read | Filtered by the allowlist |
| `search_work_items` | read | Type / state / assignee / title filters, built into escaped WIQL |
| `get_work_item` | read | Full detail, HTML description stripped |
| `list_pull_requests` / `get_pull_request` | read | Reviewers + votes, linked work items, changed files |
| `list_pipelines` / `list_pipeline_runs` | read | |
| `create_work_item` | write | |
| `update_work_item` | write | Protected fields refused. Dry run shows a field diff. |
| `add_work_item_comment` / `add_pull_request_comment` | write | |
| `run_pipeline` | destructive | Protected branches refused before approval is even requested |
| `delete_work_item` | destructive | Moves to the recycle bin |
| `guard_get_policy` / `guard_get_audit_log` | read | Lets the agent explain what it may do and what it did |

Tools carry MCP annotations (`readOnlyHint`, `destructiveHint`) so clients can apply their own confirmations on top.

### The approval flow

```jsonc
// 1. agent β†’ delete_work_item { "project": "Atlas", "id": 106 }
{ "outcome": "pending_approval",
  "preview": { "summary": "Delete Task #106 \"Add p95 latency alert for /charges\" (moves to recycle bin)" },
  "approvalToken": "approve-k3x9q2ma",
  "message": "Approval required. Show this preview to the user..." }

// 2. user says yes β†’ agent retries with the token
// agent β†’ delete_work_item { "project": "Atlas", "id": 106, "confirm": "approve-k3x9q2ma" }
{ "id": 106, "deleted": true }

// Same token, different id β†’ denied. Same token again β†’ denied. After 5 minutes β†’ denied.
```

## Development

```bash
npm install
npm test              # 29 tests: engine, REST client request shapes, end-to-end over stdio
npm run dev           # start the server on mock data
npm run site:dev      # playground at http://localhost:5173/ado-guard-mcp/
```

```
src/
  core/          browser-safe: engine, policy, tools, redaction, limits, audit, mock org
  node/          REST client (Azure DevOps 7.1), config loader, file audit sink
  server.ts      MCP adapter (registerTool + annotations)
  index.ts       stdio entry point
site/            Vite + React playground deployed to GitHub Pages
test/            Vitest suites, including a real MCP client ↔ server round trip
```

## Security notes

- The guard is a **second line of defence**. Scope your PAT (e.g. *Work Items: Read & Write* only) first.
- The PAT is only sent to Azure DevOps. It's never included in tool output or audit entries.
- Approval tokens only help if a human actually reviews the preview. MCP clients that show tool results to the
  user (Claude Desktop, VS Code) make this natural.

## Roadmap

- [ ] MCP elicitation for in-client approve/reject buttons where supported
- [ ] Wiki and repo file tools behind the same policy
- [ ] Per-tool rate limits and a time-window policy (e.g. no pipeline runs on Fridays)
- [ ] Publish to npm

---

Built by **[Malik Abdul Basit](https://abdulbasit7010.github.io)**. I build enterprise platforms and AI developer tooling.

TDQS

A3.6/5.0

Scored across 9 tools

Disambiguation5/5

Each tool targets a distinct resource and action: list vs. get pairs (work items, PRs) are clearly differentiated, and search_work_items vs. get_work_item are explicitly distinguished in the descriptions. The two guard_ tools (audit log, policy) are also unambiguous.

Naming Consistency5/5

All tools follow a consistent snake_case verb_noun pattern (list_projects, get_work_item, search_work_items, list_pipeline_runs). The guard_ prefix on two tools acts as a clear namespace rather than an inconsistency.

Tool Count5/5

Nine tools is well-scoped for a guarded read surface over Azure DevOps. Each tool earns its place with no redundant or filler operations.

Completeness3/5

The read surface covers projects, work items, PRs and pipelines reasonably, but there are notable gaps: no repository listing, no single-pipeline or single-run detail, and no write operations (create/update/comment) anywhere. Some of this may be intentional for a guardrail server, but agents will hit dead ends for common detail and mutation workflows.

Maintenance

ActivityMaintained
ResponsivenessNo issues