mcp-ado-browser 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/src/ado/archive.js +69 -0
- package/dist/src/ado/client.js +478 -0
- package/dist/src/ado/hosts.js +36 -0
- package/dist/src/ado/schemas.js +178 -0
- package/dist/src/ado/versions.js +52 -0
- package/dist/src/browser/auth-detect.js +47 -0
- package/dist/src/browser/session.js +201 -0
- package/dist/src/cache/sqlite-cache.js +66 -0
- package/dist/src/cache/types.js +1 -0
- package/dist/src/cli.js +50 -0
- package/dist/src/config.js +70 -0
- package/dist/src/errors.js +88 -0
- package/dist/src/index.js +73 -0
- package/dist/src/logger.js +22 -0
- package/dist/src/mock/mock-ado-server.js +108 -0
- package/dist/src/runtime.js +155 -0
- package/dist/src/scrub.js +38 -0
- package/dist/src/server.js +51 -0
- package/dist/src/tools/defs.js +128 -0
- package/dist/src/tools/errors.js +7 -0
- package/dist/src/transport/mock-transport.js +73 -0
- package/dist/src/transport/types.js +8 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VMargan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# mcp-ado-browser
|
|
2
|
+
|
|
3
|
+
> Azure DevOps for MCP — via your browser, not a PAT.
|
|
4
|
+
|
|
5
|
+
An **MCP (stdio) server** that gives **read-only access to Azure DevOps using only your
|
|
6
|
+
existing browser session** — **no PAT, no Azure CLI, no official ADO MCP**, no
|
|
7
|
+
credential provider. The only source of authentication is the cookie session of a real
|
|
8
|
+
browser, driven by Playwright on an **isolated, dedicated profile**.
|
|
9
|
+
|
|
10
|
+
It is **org-wide by default**: only the organization is required, and it browses **every
|
|
11
|
+
project, repo and feed you can access**.
|
|
12
|
+
|
|
13
|
+
Data is fetched via `page.evaluate(() => fetch(...))` executed **inside the
|
|
14
|
+
`dev.azure.com` page context** (same-origin), so session cookies attach automatically
|
|
15
|
+
and you get **JSON** back — never DOM scraping for data the REST API provides. Every
|
|
16
|
+
request carries `X-TFS-FedAuthRedirect: Suppress` so a dead session returns a clean
|
|
17
|
+
`401` instead of an HTML login page.
|
|
18
|
+
|
|
19
|
+
## Why these choices (restricted-environment friendly)
|
|
20
|
+
|
|
21
|
+
| Concern | Decision |
|
|
22
|
+
|---|---|
|
|
23
|
+
| No Playwright browser download | `playwright-core` + `channel: 'chrome'`/`'msedge'` uses an already-installed browser; nothing is downloaded. |
|
|
24
|
+
| SQLite without a native build | `node:sqlite` (built into Node ≥ 22.5) — zero compilation. |
|
|
25
|
+
| One package, one binary | The MCP server and the `authenticate` mechanism ship in the same package and the same `npx` binary. |
|
|
26
|
+
| No hardcoded values | org/project/ids come from flags/env or discovery; api-versions live only in `src/ado/versions.ts`. |
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
```mermaid
|
|
31
|
+
flowchart TD
|
|
32
|
+
A["authenticate<br/>(visible, chromeless window)"] -->|"sign in once · MFA"| P[("session cookies<br/>persisted on an isolated profile")]
|
|
33
|
+
P -. reused .-> W["headless work session"]
|
|
34
|
+
|
|
35
|
+
MC["MCP client<br/>(Claude / Cursor / …)"] -->|"tools/call"| SRV["mcp-ado-browser<br/>(stdio MCP server)"]
|
|
36
|
+
SRV --> W
|
|
37
|
+
W -->|"page.evaluate(fetch)<br/>same-origin, cookies attached"| ADO["dev.azure.com · feeds · pkgs<br/>(your real session)"]
|
|
38
|
+
ADO -->|"JSON"| W
|
|
39
|
+
W --> SRV
|
|
40
|
+
SRV <-->|"TTL + Rev freshness"| DB[("SQLite cache")]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
1. **Authentication is your browser, not a token.** `authenticate` opens a real,
|
|
44
|
+
visible browser window on a **dedicated, isolated profile** (never your daily
|
|
45
|
+
browser). You sign in normally (MFA included). The tool detects success by polling
|
|
46
|
+
an authenticated endpoint, then persists the session cookies on disk. No PAT or
|
|
47
|
+
token is ever created or stored.
|
|
48
|
+
2. **Work runs headless.** Subsequent runs launch the same profile **headless** and
|
|
49
|
+
reuse the persisted cookies — no window, no re-login until the session expires.
|
|
50
|
+
3. **Data comes back as JSON, not scraped HTML.** Each tool runs
|
|
51
|
+
`fetch(...)` **inside the `dev.azure.com` page context** (same-origin), so the
|
|
52
|
+
session cookies attach automatically. Every request sends
|
|
53
|
+
`X-TFS-FedAuthRedirect: Suppress`, so an expired session returns a clean `401`
|
|
54
|
+
(surfaced as a structured `AUTH_REQUIRED` error) instead of an HTML login page.
|
|
55
|
+
Cross-host services (feeds / packages) use the same browser cookie jar.
|
|
56
|
+
4. **Responses are cached** in a local SQLite DB (`node:sqlite`) with a configurable
|
|
57
|
+
TTL. On a stale hit, a cheap freshness check (`System.Rev` for work items) avoids
|
|
58
|
+
re-downloading unchanged data.
|
|
59
|
+
5. **When the session dies**, tools fail fast with `AUTH_REQUIRED` — just re-run
|
|
60
|
+
`authenticate` and continue.
|
|
61
|
+
|
|
62
|
+
## Getting started
|
|
63
|
+
|
|
64
|
+
**Prerequisites:** Node ≥ 22.5 and Google Chrome (or Microsoft Edge) installed. You do
|
|
65
|
+
**not** need a PAT, the Azure CLI, or any admin setup.
|
|
66
|
+
|
|
67
|
+
Setup is **two steps**:
|
|
68
|
+
|
|
69
|
+
1. **Register the server** in your MCP client (one config entry — see your client below).
|
|
70
|
+
2. **Sign in once** — just ask your assistant: *“authenticate to Azure DevOps”*. The
|
|
71
|
+
built-in `authenticate` tool opens a visible browser window; you log in (MFA), and the
|
|
72
|
+
session is persisted. (No separate terminal command needed.) From then on everything
|
|
73
|
+
runs headless until the session expires — then just ask it to authenticate again.
|
|
74
|
+
|
|
75
|
+
The command every client runs is the same:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx -y mcp-ado-browser --org <your-org>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Config is passed as CLI flags (`--org`, `--project`, …) or env vars (`ADO_ORG`, …);
|
|
82
|
+
flags win. Then ask things like *“list my active pull requests”*, *“show work item 1234
|
|
83
|
+
and its linked PR”*, or *“what feeds and packages are in this org?”*.
|
|
84
|
+
|
|
85
|
+
> Tip: prefer per-user/local config (not committed) so your org name doesn't land in a
|
|
86
|
+
> shared repo. Or omit `--org` from a committed config and set `ADO_ORG` in your env.
|
|
87
|
+
|
|
88
|
+
## Use it from your MCP client
|
|
89
|
+
|
|
90
|
+
<details open>
|
|
91
|
+
<summary><b>Claude Code</b></summary>
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
claude mcp add ado --scope local -- npx -y mcp-ado-browser --org <your-org>
|
|
95
|
+
```
|
|
96
|
+
Then ask Claude to *“authenticate to Azure DevOps”*.
|
|
97
|
+
</details>
|
|
98
|
+
|
|
99
|
+
<details>
|
|
100
|
+
<summary><b>Claude Desktop</b> — <code>claude_desktop_config.json</code></summary>
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"ado": {
|
|
106
|
+
"command": "npx",
|
|
107
|
+
"args": ["-y", "mcp-ado-browser", "--org", "<your-org>"]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
</details>
|
|
113
|
+
|
|
114
|
+
<details>
|
|
115
|
+
<summary><b>GitHub Copilot (VS Code)</b> — <code>.vscode/mcp.json</code></summary>
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"servers": {
|
|
120
|
+
"ado": {
|
|
121
|
+
"type": "stdio",
|
|
122
|
+
"command": "npx",
|
|
123
|
+
"args": ["-y", "mcp-ado-browser", "--org", "<your-org>"]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
Open Copilot Chat in **Agent** mode and pick the `ado` tools. (Avoid committing your org —
|
|
129
|
+
use `${env:ADO_ORG}` or a personal config.)
|
|
130
|
+
</details>
|
|
131
|
+
|
|
132
|
+
<details>
|
|
133
|
+
<summary><b>Cursor</b> — <code>~/.cursor/mcp.json</code> (or <code>.cursor/mcp.json</code>)</summary>
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"mcpServers": {
|
|
138
|
+
"ado": {
|
|
139
|
+
"command": "npx",
|
|
140
|
+
"args": ["-y", "mcp-ado-browser", "--org", "<your-org>"]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
</details>
|
|
146
|
+
|
|
147
|
+
<details>
|
|
148
|
+
<summary><b>Codex CLI</b> — <code>~/.codex/config.toml</code></summary>
|
|
149
|
+
|
|
150
|
+
```toml
|
|
151
|
+
[mcp_servers.ado]
|
|
152
|
+
command = "npx"
|
|
153
|
+
args = ["-y", "mcp-ado-browser", "--org", "<your-org>"]
|
|
154
|
+
```
|
|
155
|
+
</details>
|
|
156
|
+
|
|
157
|
+
After registering, trigger sign-in **from the chat** (*“authenticate to Azure DevOps”*),
|
|
158
|
+
which runs the `authenticate` tool. Prefer a terminal instead? `npx -y mcp-ado-browser
|
|
159
|
+
authenticate --org <your-org>` does the same thing. Tools return a structured
|
|
160
|
+
`AUTH_REQUIRED` error when the session expires — re-authenticate and continue.
|
|
161
|
+
|
|
162
|
+
## Tools (`tools/list`)
|
|
163
|
+
|
|
164
|
+
| Tool | What it does |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `list_projects` | All projects you can access (org-wide). |
|
|
167
|
+
| `list_repositories` | All Git repos across the org (or one project). |
|
|
168
|
+
| `search_work_items` | WIQL (org-wide by default) or full-text (almsearch); `project` to scope. |
|
|
169
|
+
| `get_work_item` | Work item with `$expand=all` + `relations` (hierarchy, Related, PR ArtifactLink resolved). |
|
|
170
|
+
| `get_work_item_comments` | The separate comments endpoint (project derived automatically). |
|
|
171
|
+
| `get_comment_details` | A comment **plus** its downloaded attachments (size, sha256). |
|
|
172
|
+
| `search_pull_requests` | PRs org-wide, by repo, or by project; filter by status/author/target. |
|
|
173
|
+
| `get_pull_request` | Metadata, branches, reviewers, linked work items (repo by id **or name**). |
|
|
174
|
+
| `get_pull_request_comments` | Threads, distinguishing **system vs human**. |
|
|
175
|
+
| `search_feeds` | Artifacts feeds → packages → versions. |
|
|
176
|
+
| `download_artifact` | `.nupkg`/`.tgz` from a feed (cross-host `pkgs.dev.azure.com`), with archive-integrity validation. |
|
|
177
|
+
| `authenticate` | Opens a visible browser for interactive sign-in (MFA); persists the session. Run it once, or whenever a tool returns `AUTH_REQUIRED`. |
|
|
178
|
+
|
|
179
|
+
## Commands
|
|
180
|
+
|
|
181
|
+
The single `npx mcp-ado-browser` binary has a few subcommands:
|
|
182
|
+
|
|
183
|
+
| Command | What it does |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `npx mcp-ado-browser --org <org>` | Start the MCP stdio server (default). |
|
|
186
|
+
| `… authenticate --org <org>` | Interactive sign-in (visible browser). Same as the `authenticate` tool. |
|
|
187
|
+
| `… status --org <org>` | Show the profile/cache paths, the org, and whether the session is signed in (and as who). |
|
|
188
|
+
| `… logout` | Clear the persisted session **and** the cache (a local sign-out). No org needed. |
|
|
189
|
+
|
|
190
|
+
> Switching org with the **same** account needs nothing special — just change `--org`; one
|
|
191
|
+
> sign-in covers every org that account can access. A different account → `logout` first,
|
|
192
|
+
> then `authenticate` against the other org.
|
|
193
|
+
|
|
194
|
+
## Where it stores things
|
|
195
|
+
|
|
196
|
+
Everything is local to your machine, under a single dedicated folder (mode `700`, never
|
|
197
|
+
committed). Nothing is hosted remotely — the server is a local process spawned by your MCP
|
|
198
|
+
client over stdio.
|
|
199
|
+
|
|
200
|
+
| What | Path (default) |
|
|
201
|
+
|---|---|
|
|
202
|
+
| Browser session (cookies) | **macOS/Linux:** `~/.mcp-ado-browser/profile/` · **Windows:** `C:\Users\<you>\.mcp-ado-browser\profile\` |
|
|
203
|
+
| SQLite cache | `…/.mcp-ado-browser/cache.sqlite` |
|
|
204
|
+
| Package code (npx cache) | **macOS/Linux:** `~/.npm/_npx/<hash>/…/mcp-ado-browser` · **Windows:** `…\AppData\Local\npm-cache\_npx\<hash>\…` (see `npm config get cache`) |
|
|
205
|
+
|
|
206
|
+
Reset everything (forces re-login): `logout`, or `rm -rf ~/.mcp-ado-browser`.
|
|
207
|
+
|
|
208
|
+
## Configuration
|
|
209
|
+
|
|
210
|
+
| Flag | Env | Default | Meaning |
|
|
211
|
+
|---|---|---|---|
|
|
212
|
+
| `--org` | `ADO_ORG` | — | Organization (**required**). |
|
|
213
|
+
| `--project` | `ADO_PROJECT` | — | Default project scope (optional; org-wide otherwise). |
|
|
214
|
+
| `--user-data-dir` | `ADO_USER_DATA_DIR` | `~/.mcp-ado-browser/profile` | Isolated persistent browser profile. |
|
|
215
|
+
| `--channel` | `ADO_BROWSER_CHANNEL` | `chrome` | `chrome` or `msedge`. |
|
|
216
|
+
| `--cache-ttl` | `ADO_CACHE_TTL_SECONDS` | `900` | Global cache TTL. Per-resource: `ADO_CACHE_TTL_WORKITEM=60`. |
|
|
217
|
+
| `--api-version` | `ADO_API_VERSION` | discovery/defaults | Force an api-version for all areas. |
|
|
218
|
+
| `--no-app-window` | `ADO_APP_WINDOW=0` | app mode | Use a normal browser window for sign-in. |
|
|
219
|
+
| `--headed` | `ADO_HEADLESS=0` | headless | Run work with a visible window. |
|
|
220
|
+
|
|
221
|
+
## Development & verification
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
npm install
|
|
225
|
+
npm run build
|
|
226
|
+
npm run verify # all offline gates (browser stack, MCP, tools, cache, artifacts, no-hardcoding)
|
|
227
|
+
npm run verify:live # adds the live acceptance pass against real Azure DevOps
|
|
228
|
+
npm run scan:secrets # pre-push secret / sensitive-data scan
|
|
229
|
+
npm run demo:live # drive the real stdio server as an MCP client (env-driven)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`npm run verify` prints a detailed report, gate by gate, assertion by assertion.
|
|
233
|
+
`BLOCKED_ON_AUTH` is transitory: the run is not done until the live pass is green; the
|
|
234
|
+
only tolerated terminal exclusion is `EMPIRICALLY_BLOCKED` (with evidence), for the
|
|
235
|
+
cross-host artifact download only.
|
|
236
|
+
|
|
237
|
+
## Security & privacy
|
|
238
|
+
|
|
239
|
+
- Authentication is **only** your real browser session on a dedicated, isolated profile
|
|
240
|
+
— no PAT or token is ever created, stored, or transmitted by this tool.
|
|
241
|
+
- The session lives in `~/.mcp-ado-browser/profile` (machine-local, gitignored).
|
|
242
|
+
- Fixtures and reports are anonymized; `npm run scan:secrets` blocks pushes that would
|
|
243
|
+
leak personal/org data or secrets (also enforced in CI).
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
MIT © VMargan
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Archive integrity validation for downloaded artifacts (Phase 3 gate).
|
|
3
|
+
*
|
|
4
|
+
* - .nupkg is a ZIP: must start with the PK local-file signature and contain a
|
|
5
|
+
* `.nuspec` entry (proves it's a real NuGet package, reusable for re-push).
|
|
6
|
+
* - .tgz is gzip(tar): gunzip then confirm the tar stream contains `package.json`.
|
|
7
|
+
*
|
|
8
|
+
* Uses only node:zlib — no third-party archive deps.
|
|
9
|
+
*/
|
|
10
|
+
import * as zlib from "node:zlib";
|
|
11
|
+
export function validateArchive(protocol, data) {
|
|
12
|
+
return protocol === "nuget" ? validateNupkg(data) : validateTgz(data);
|
|
13
|
+
}
|
|
14
|
+
function validateNupkg(data) {
|
|
15
|
+
if (data.length < 4 || data[0] !== 0x50 || data[1] !== 0x4b) {
|
|
16
|
+
return { valid: false, detail: "not a ZIP archive (missing PK signature)" };
|
|
17
|
+
}
|
|
18
|
+
// Scan the central directory file names for a .nuspec entry.
|
|
19
|
+
const hasNuspec = scanZipForExtension(data, ".nuspec");
|
|
20
|
+
return hasNuspec
|
|
21
|
+
? { valid: true, detail: "valid .nupkg (ZIP with .nuspec entry)" }
|
|
22
|
+
: { valid: false, detail: "ZIP archive without a .nuspec entry" };
|
|
23
|
+
}
|
|
24
|
+
function validateTgz(data) {
|
|
25
|
+
if (data.length < 2 || data[0] !== 0x1f || data[1] !== 0x8b) {
|
|
26
|
+
return { valid: false, detail: "not a gzip stream (missing 1f 8b magic)" };
|
|
27
|
+
}
|
|
28
|
+
let tar;
|
|
29
|
+
try {
|
|
30
|
+
tar = zlib.gunzipSync(data);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
return { valid: false, detail: `gunzip failed: ${String(e)}` };
|
|
34
|
+
}
|
|
35
|
+
// npm tarballs put files under "package/"; the manifest is package/package.json.
|
|
36
|
+
const hasManifest = tarContainsName(tar, "package.json");
|
|
37
|
+
return hasManifest
|
|
38
|
+
? { valid: true, detail: "valid .tgz (gzip+tar containing package.json)" }
|
|
39
|
+
: { valid: false, detail: "gzip/tar without a package.json entry" };
|
|
40
|
+
}
|
|
41
|
+
/** Walk ZIP central-directory headers (PK\x01\x02) and test each file name. */
|
|
42
|
+
function scanZipForExtension(buf, ext) {
|
|
43
|
+
const CEN = 0x02014b50; // central directory header signature
|
|
44
|
+
for (let i = 0; i + 46 <= buf.length; i++) {
|
|
45
|
+
if (buf.readUInt32LE(i) === CEN) {
|
|
46
|
+
const nameLen = buf.readUInt16LE(i + 28);
|
|
47
|
+
const name = buf.toString("utf8", i + 46, i + 46 + nameLen);
|
|
48
|
+
if (name.toLowerCase().endsWith(ext))
|
|
49
|
+
return true;
|
|
50
|
+
i += 46 + nameLen - 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Fallback: some minimal zips — scan raw bytes for the extension string.
|
|
54
|
+
return buf.toString("latin1").toLowerCase().includes(ext);
|
|
55
|
+
}
|
|
56
|
+
/** Walk 512-byte tar headers and test each entry name for a suffix. */
|
|
57
|
+
function tarContainsName(tar, suffix) {
|
|
58
|
+
for (let off = 0; off + 512 <= tar.length;) {
|
|
59
|
+
const nameRaw = tar.toString("utf8", off, off + 100).replace(/\0.*$/, "");
|
|
60
|
+
if (nameRaw === "")
|
|
61
|
+
break; // two zero blocks => end of archive
|
|
62
|
+
if (nameRaw.endsWith(suffix))
|
|
63
|
+
return true;
|
|
64
|
+
const sizeStr = tar.toString("ascii", off + 124, off + 136).replace(/\0.*$/, "").trim();
|
|
65
|
+
const size = parseInt(sizeStr, 8) || 0;
|
|
66
|
+
off += 512 + Math.ceil(size / 512) * 512;
|
|
67
|
+
}
|
|
68
|
+
return tar.toString("latin1").includes(suffix);
|
|
69
|
+
}
|