az-devops-cli-mcp 1.0.2__tar.gz
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.
- az_devops_cli_mcp-1.0.2/CHANGELOG.md +19 -0
- az_devops_cli_mcp-1.0.2/LICENSE +21 -0
- az_devops_cli_mcp-1.0.2/MANIFEST.in +4 -0
- az_devops_cli_mcp-1.0.2/PKG-INFO +166 -0
- az_devops_cli_mcp-1.0.2/README.md +133 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp/__init__.py +54 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp/__main__.py +6 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp/config.py +50 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp/core.py +76 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp/server.py +351 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp/tools/__init__.py +0 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp/tools/repos.py +124 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp/tools/work_items.py +167 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp.egg-info/PKG-INFO +166 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp.egg-info/SOURCES.txt +20 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp.egg-info/dependency_links.txt +1 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp.egg-info/entry_points.txt +2 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp.egg-info/requires.txt +11 -0
- az_devops_cli_mcp-1.0.2/az_devops_cli_mcp.egg-info/top_level.txt +1 -0
- az_devops_cli_mcp-1.0.2/pyproject.toml +46 -0
- az_devops_cli_mcp-1.0.2/setup.cfg +4 -0
- az_devops_cli_mcp-1.0.2/tests/test_core.py +93 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `az-devops-cli-mcp` will be documented here.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
5
|
+
Versioning follows [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## [1.0.0] — Initial Release
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- 13 MCP tools covering Azure DevOps Boards and Repos
|
|
13
|
+
- Work items: create, get, update, set_iteration, query, link, verify relations, comment
|
|
14
|
+
- Repos/PRs: list, get, create, link to work item, list branches
|
|
15
|
+
- stdio MCP transport — compatible with Claude Code and Codex CLI
|
|
16
|
+
- TOON (Token-Oriented Object Notation) response format
|
|
17
|
+
- Destructive command guard (delete/remove/destroy blocked by default)
|
|
18
|
+
- Auto org detection from `az devops configure` or `AZURE_DEVOPS_ORG` env var
|
|
19
|
+
- AG2 tools library interface via `from az_devops_cli_mcp import ALL_TOOLS`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 LytStore Team
|
|
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.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: az-devops-cli-mcp
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Azure DevOps CLI MCP server for Claude Code and Codex CLI — boards, work items, repos, PRs
|
|
5
|
+
Author: LytStore Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PraiseSinkamba/az-devops-cli-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/PraiseSinkamba/az-devops-cli-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/PraiseSinkamba/az-devops-cli-mcp/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/PraiseSinkamba/az-devops-cli-mcp/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: azure,devops,mcp,claude,codex,boards,work-items
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: mcp>=1.0.0
|
|
24
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
25
|
+
Provides-Extra: ag2
|
|
26
|
+
Requires-Dist: pyautogen>=0.2.0; extra == "ag2"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
30
|
+
Requires-Dist: build; extra == "dev"
|
|
31
|
+
Requires-Dist: twine; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# az-devops-cli-mcp
|
|
35
|
+
|
|
36
|
+
Azure DevOps MCP tools for **Claude Code** and **Codex CLI** — boards, work items, repos, and PRs.
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/az-devops-cli-mcp/)
|
|
39
|
+
[](https://github.com/PraiseSinkamba/az-devops-cli-mcp/actions)
|
|
40
|
+
[](https://pypi.org/project/az-devops-cli-mcp/)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Install & Register
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# 1. Install
|
|
48
|
+
pip install az-devops-cli-mcp
|
|
49
|
+
|
|
50
|
+
# 2. Login (once per machine)
|
|
51
|
+
az login
|
|
52
|
+
az devops configure --defaults organization=https://dev.azure.com/YOUR_ORG
|
|
53
|
+
|
|
54
|
+
# 3. Register with Claude Code
|
|
55
|
+
claude mcp add az-devops-cli-mcp -- python -m az_devops_cli_mcp.server
|
|
56
|
+
|
|
57
|
+
# 4. Register with Codex CLI
|
|
58
|
+
codex mcp add az-devops-cli-mcp -- python -m az_devops_cli_mcp.server
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Restart Claude Code or Codex CLI. All 13 tools are ready.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## How It Works
|
|
66
|
+
|
|
67
|
+
The server runs as a **stdio process** — spawned on demand by the client, no ports or background services needed.
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Claude Code / Codex CLI
|
|
71
|
+
| spawns on demand
|
|
72
|
+
v
|
|
73
|
+
python -m az_devops_cli_mcp.server (stdin/stdout)
|
|
74
|
+
|
|
|
75
|
+
v
|
|
76
|
+
az CLI (your az login session)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Available Tools (13 total)
|
|
82
|
+
|
|
83
|
+
### Work Items
|
|
84
|
+
| Tool | What it does |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `create_work_item` | Create Task, Bug, User Story, Epic |
|
|
87
|
+
| `get_work_item` | Get item by ID |
|
|
88
|
+
| `update_work_item` | Update state, assignment, title |
|
|
89
|
+
| `set_iteration` | Assign to sprint (required for Sprint board) |
|
|
90
|
+
| `query_work_items` | WIQL query for bulk lookups |
|
|
91
|
+
| `link_work_items` | Link items (Child, Parent, Related) |
|
|
92
|
+
| `get_work_item_relations` | Verify hierarchy after linking |
|
|
93
|
+
| `add_comment` | Add discussion comment / agent log |
|
|
94
|
+
|
|
95
|
+
### Repos / PRs
|
|
96
|
+
| Tool | What it does |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `list_prs` | List PRs by status |
|
|
99
|
+
| `get_pr` | Get PR details |
|
|
100
|
+
| `create_pr` | Open a new PR |
|
|
101
|
+
| `link_pr_to_work_item` | Trace PR to work item |
|
|
102
|
+
| `list_branches` | List repo branches |
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## AG2 Usage (optional)
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from autogen import ConversableAgent
|
|
110
|
+
from az_devops_cli_mcp import ALL_TOOLS, WORK_ITEM_TOOLS, REPO_TOOLS
|
|
111
|
+
|
|
112
|
+
agent = ConversableAgent(
|
|
113
|
+
name="devops_agent",
|
|
114
|
+
tools=ALL_TOOLS,
|
|
115
|
+
system_message="You manage Azure DevOps for LytStore..."
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Configuration
|
|
122
|
+
|
|
123
|
+
| Method | How |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `az devops configure` | Preferred — org set globally for your machine |
|
|
126
|
+
| `AZURE_DEVOPS_ORG` env var | Per-session override |
|
|
127
|
+
| `.env` file | `AZURE_DEVOPS_ORG=https://dev.azure.com/YOUR_ORG` |
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Safety
|
|
132
|
+
|
|
133
|
+
Destructive commands (`delete`, `remove`, `destroy`) are blocked at the core layer.
|
|
134
|
+
The agent returns an error and requires explicit user confirmation.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Manual MCP Config (fallback)
|
|
139
|
+
|
|
140
|
+
If `claude mcp add` / `codex mcp add` aren't available, add this to your config file manually.
|
|
141
|
+
|
|
142
|
+
**Claude Code** — `~/.claude/claude_desktop_config.json`:
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"mcpServers": {
|
|
146
|
+
"az-devops-cli-mcp": {
|
|
147
|
+
"type": "stdio",
|
|
148
|
+
"command": "python",
|
|
149
|
+
"args": ["-m", "az_devops_cli_mcp.server"]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Codex CLI** — `~/.codex/config.json`: same block.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Contributing
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
git clone https://github.com/PraiseSinkamba/az-devops-cli-mcp
|
|
163
|
+
cd az-devops-cli-mcp
|
|
164
|
+
pip install -e ".[dev]"
|
|
165
|
+
pytest tests/
|
|
166
|
+
```
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# az-devops-cli-mcp
|
|
2
|
+
|
|
3
|
+
Azure DevOps MCP tools for **Claude Code** and **Codex CLI** — boards, work items, repos, and PRs.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/az-devops-cli-mcp/)
|
|
6
|
+
[](https://github.com/PraiseSinkamba/az-devops-cli-mcp/actions)
|
|
7
|
+
[](https://pypi.org/project/az-devops-cli-mcp/)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Install & Register
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 1. Install
|
|
15
|
+
pip install az-devops-cli-mcp
|
|
16
|
+
|
|
17
|
+
# 2. Login (once per machine)
|
|
18
|
+
az login
|
|
19
|
+
az devops configure --defaults organization=https://dev.azure.com/YOUR_ORG
|
|
20
|
+
|
|
21
|
+
# 3. Register with Claude Code
|
|
22
|
+
claude mcp add az-devops-cli-mcp -- python -m az_devops_cli_mcp.server
|
|
23
|
+
|
|
24
|
+
# 4. Register with Codex CLI
|
|
25
|
+
codex mcp add az-devops-cli-mcp -- python -m az_devops_cli_mcp.server
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Restart Claude Code or Codex CLI. All 13 tools are ready.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## How It Works
|
|
33
|
+
|
|
34
|
+
The server runs as a **stdio process** — spawned on demand by the client, no ports or background services needed.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Claude Code / Codex CLI
|
|
38
|
+
| spawns on demand
|
|
39
|
+
v
|
|
40
|
+
python -m az_devops_cli_mcp.server (stdin/stdout)
|
|
41
|
+
|
|
|
42
|
+
v
|
|
43
|
+
az CLI (your az login session)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Available Tools (13 total)
|
|
49
|
+
|
|
50
|
+
### Work Items
|
|
51
|
+
| Tool | What it does |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `create_work_item` | Create Task, Bug, User Story, Epic |
|
|
54
|
+
| `get_work_item` | Get item by ID |
|
|
55
|
+
| `update_work_item` | Update state, assignment, title |
|
|
56
|
+
| `set_iteration` | Assign to sprint (required for Sprint board) |
|
|
57
|
+
| `query_work_items` | WIQL query for bulk lookups |
|
|
58
|
+
| `link_work_items` | Link items (Child, Parent, Related) |
|
|
59
|
+
| `get_work_item_relations` | Verify hierarchy after linking |
|
|
60
|
+
| `add_comment` | Add discussion comment / agent log |
|
|
61
|
+
|
|
62
|
+
### Repos / PRs
|
|
63
|
+
| Tool | What it does |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `list_prs` | List PRs by status |
|
|
66
|
+
| `get_pr` | Get PR details |
|
|
67
|
+
| `create_pr` | Open a new PR |
|
|
68
|
+
| `link_pr_to_work_item` | Trace PR to work item |
|
|
69
|
+
| `list_branches` | List repo branches |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## AG2 Usage (optional)
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from autogen import ConversableAgent
|
|
77
|
+
from az_devops_cli_mcp import ALL_TOOLS, WORK_ITEM_TOOLS, REPO_TOOLS
|
|
78
|
+
|
|
79
|
+
agent = ConversableAgent(
|
|
80
|
+
name="devops_agent",
|
|
81
|
+
tools=ALL_TOOLS,
|
|
82
|
+
system_message="You manage Azure DevOps for LytStore..."
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
| Method | How |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `az devops configure` | Preferred — org set globally for your machine |
|
|
93
|
+
| `AZURE_DEVOPS_ORG` env var | Per-session override |
|
|
94
|
+
| `.env` file | `AZURE_DEVOPS_ORG=https://dev.azure.com/YOUR_ORG` |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Safety
|
|
99
|
+
|
|
100
|
+
Destructive commands (`delete`, `remove`, `destroy`) are blocked at the core layer.
|
|
101
|
+
The agent returns an error and requires explicit user confirmation.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Manual MCP Config (fallback)
|
|
106
|
+
|
|
107
|
+
If `claude mcp add` / `codex mcp add` aren't available, add this to your config file manually.
|
|
108
|
+
|
|
109
|
+
**Claude Code** — `~/.claude/claude_desktop_config.json`:
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"mcpServers": {
|
|
113
|
+
"az-devops-cli-mcp": {
|
|
114
|
+
"type": "stdio",
|
|
115
|
+
"command": "python",
|
|
116
|
+
"args": ["-m", "az_devops_cli_mcp.server"]
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Codex CLI** — `~/.codex/config.json`: same block.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Contributing
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git clone https://github.com/PraiseSinkamba/az-devops-cli-mcp
|
|
130
|
+
cd az-devops-cli-mcp
|
|
131
|
+
pip install -e ".[dev]"
|
|
132
|
+
pytest tests/
|
|
133
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
az-devops-cli-mcp
|
|
3
|
+
===============
|
|
4
|
+
Azure DevOps CLI MCP server — boards, work items, repos, PRs.
|
|
5
|
+
|
|
6
|
+
Usage in your agent:
|
|
7
|
+
from az_devops_cli_mcp import ALL_TOOLS, WORK_ITEM_TOOLS, REPO_TOOLS
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from az_devops_cli_mcp.tools.work_items import (
|
|
11
|
+
create_work_item,
|
|
12
|
+
get_work_item,
|
|
13
|
+
update_work_item,
|
|
14
|
+
set_iteration,
|
|
15
|
+
query_work_items,
|
|
16
|
+
link_work_items,
|
|
17
|
+
get_work_item_relations,
|
|
18
|
+
add_comment,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from az_devops_cli_mcp.tools.repos import (
|
|
22
|
+
list_prs,
|
|
23
|
+
get_pr,
|
|
24
|
+
create_pr,
|
|
25
|
+
link_pr_to_work_item,
|
|
26
|
+
list_branches,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
WORK_ITEM_TOOLS = [
|
|
30
|
+
create_work_item,
|
|
31
|
+
get_work_item,
|
|
32
|
+
update_work_item,
|
|
33
|
+
set_iteration,
|
|
34
|
+
query_work_items,
|
|
35
|
+
link_work_items,
|
|
36
|
+
get_work_item_relations,
|
|
37
|
+
add_comment,
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
REPO_TOOLS = [
|
|
41
|
+
list_prs,
|
|
42
|
+
get_pr,
|
|
43
|
+
create_pr,
|
|
44
|
+
link_pr_to_work_item,
|
|
45
|
+
list_branches,
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
ALL_TOOLS = WORK_ITEM_TOOLS + REPO_TOOLS
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"ALL_TOOLS",
|
|
52
|
+
"WORK_ITEM_TOOLS",
|
|
53
|
+
"REPO_TOOLS",
|
|
54
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
az_devops_cli_mcp.config
|
|
3
|
+
======================
|
|
4
|
+
Resolves the Azure DevOps org URL in this priority order:
|
|
5
|
+
|
|
6
|
+
1. AZURE_DEVOPS_ORG environment variable (or .env file)
|
|
7
|
+
2. az devops configure --list (whatever the user already set)
|
|
8
|
+
|
|
9
|
+
This means each team member just runs:
|
|
10
|
+
az login
|
|
11
|
+
az devops configure --defaults organization=https://dev.azure.com/YOUR_ORG
|
|
12
|
+
|
|
13
|
+
...and the tools pick it up automatically.
|
|
14
|
+
No hardcoded values, no shared secrets.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import subprocess
|
|
18
|
+
import os
|
|
19
|
+
from dotenv import load_dotenv
|
|
20
|
+
|
|
21
|
+
load_dotenv() # load .env if present (optional, never required)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_org() -> str:
|
|
25
|
+
"""
|
|
26
|
+
Resolve the Azure DevOps org URL.
|
|
27
|
+
Raises RuntimeError if neither env var nor az defaults are set.
|
|
28
|
+
"""
|
|
29
|
+
# 1. Env override
|
|
30
|
+
org = os.getenv("AZURE_DEVOPS_ORG", "").strip()
|
|
31
|
+
if org:
|
|
32
|
+
return org
|
|
33
|
+
|
|
34
|
+
# 2. Read from az devops configure --list
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
"az devops configure --list",
|
|
37
|
+
shell=True, capture_output=True, text=True
|
|
38
|
+
)
|
|
39
|
+
for line in result.stdout.splitlines():
|
|
40
|
+
if line.strip().startswith("organization"):
|
|
41
|
+
_, _, value = line.partition("=")
|
|
42
|
+
org = value.strip()
|
|
43
|
+
if org:
|
|
44
|
+
return org
|
|
45
|
+
|
|
46
|
+
raise RuntimeError(
|
|
47
|
+
"No Azure DevOps org configured.\n"
|
|
48
|
+
"Run: az devops configure --defaults organization=https://dev.azure.com/YOUR_ORG\n"
|
|
49
|
+
"Or set AZURE_DEVOPS_ORG in your environment."
|
|
50
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
az_devops_cli_mcp.core
|
|
3
|
+
====================
|
|
4
|
+
Shared helpers used by all tool modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
from az_devops_cli_mcp.config import get_org
|
|
10
|
+
|
|
11
|
+
BLOCKED_COMMANDS = ["delete", "remove", "destroy"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def az(args: str) -> dict | list:
|
|
15
|
+
"""
|
|
16
|
+
Execute an az CLI command against the configured org.
|
|
17
|
+
Returns parsed JSON on success, {"error": "..."} on failure.
|
|
18
|
+
Blocks destructive commands until the user confirms.
|
|
19
|
+
"""
|
|
20
|
+
if any(cmd in args.lower() for cmd in BLOCKED_COMMANDS):
|
|
21
|
+
return {
|
|
22
|
+
"error": (
|
|
23
|
+
f"Blocked: destructive command detected in '{args}'. "
|
|
24
|
+
"Confirm with user before proceeding."
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
org = get_org()
|
|
30
|
+
except RuntimeError as e:
|
|
31
|
+
return {"error": str(e)}
|
|
32
|
+
|
|
33
|
+
result = subprocess.run(
|
|
34
|
+
f"az {args} --output json --org {org}",
|
|
35
|
+
shell=True,
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if result.returncode != 0:
|
|
41
|
+
return {"error": result.stderr.strip()}
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
return json.loads(result.stdout)
|
|
45
|
+
except json.JSONDecodeError:
|
|
46
|
+
return {"error": f"Failed to parse response: {result.stdout.strip()}"}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def toon(data: dict | list, summary: str) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Token-Oriented Object Notation:
|
|
52
|
+
Flat dict/list → key=value pipe-separated (token-efficient)
|
|
53
|
+
Nested objects → full JSON fallback
|
|
54
|
+
Error dict → [ERROR] prefix
|
|
55
|
+
Always prepends a summary line.
|
|
56
|
+
"""
|
|
57
|
+
if isinstance(data, dict) and "error" in data:
|
|
58
|
+
return f"[ERROR] {data['error']}"
|
|
59
|
+
|
|
60
|
+
def is_flat(obj: dict) -> bool:
|
|
61
|
+
return isinstance(obj, dict) and all(
|
|
62
|
+
not isinstance(v, (dict, list)) for v in obj.values()
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if isinstance(data, dict) and is_flat(data):
|
|
66
|
+
body = " | ".join(f"{k}={v}" for k, v in data.items())
|
|
67
|
+
|
|
68
|
+
elif isinstance(data, list) and all(is_flat(i) for i in data):
|
|
69
|
+
body = "(none)" if not data else "\n".join(
|
|
70
|
+
" | ".join(f"{k}={v}" for k, v in item.items())
|
|
71
|
+
for item in data
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
body = json.dumps(data, indent=2)
|
|
75
|
+
|
|
76
|
+
return f"[{summary}]\n{body}"
|