ADO Guard MCP
<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.
[](https://github.com/abdulbasit7010/ado-guard-mcp/actions/workflows/ci.yml)



### [βΆ 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
Scored across 9 tools
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.
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.
Nine tools is well-scoped for a guarded read surface over Azure DevOps. Each tool earns its place with no redundant or filler operations.
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.