azure-devops-work-items-mcp 1.0.0__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.
- azure_devops_work_items_mcp-1.0.0/.gitignore +8 -0
- azure_devops_work_items_mcp-1.0.0/LICENSE +21 -0
- azure_devops_work_items_mcp-1.0.0/PKG-INFO +74 -0
- azure_devops_work_items_mcp-1.0.0/README.md +60 -0
- azure_devops_work_items_mcp-1.0.0/pyproject.toml +28 -0
- azure_devops_work_items_mcp-1.0.0/src/azure_devops_work_items_mcp/__init__.py +3 -0
- azure_devops_work_items_mcp-1.0.0/src/azure_devops_work_items_mcp/ado_client.py +93 -0
- azure_devops_work_items_mcp-1.0.0/src/azure_devops_work_items_mcp/server.py +201 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Camylla Ribeiro
|
|
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,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: azure-devops-work-items-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MCP server for Azure DevOps work items — read, list, create, and update, with WIQL-based filtering
|
|
5
|
+
Project-URL: Repository, https://github.com/Camy-Ribeiro/azure-devops-work-items-mcp
|
|
6
|
+
Author: Camylla Ribeiro
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: azure-devops,mcp,model-context-protocol,work-items
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: mcp>=1.2.0
|
|
12
|
+
Requires-Dist: requests>=2.31.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# azure-devops-work-items-mcp
|
|
16
|
+
|
|
17
|
+
An MCP server for Azure DevOps work items: get, list/filter, create, and
|
|
18
|
+
update. Filtering supports state, work item type, assignee, and area path
|
|
19
|
+
directly, plus an open-ended `custom_filter` parameter that accepts a raw
|
|
20
|
+
WIQL WHERE-clause fragment for anything else (sprint, priority, tags, date
|
|
21
|
+
ranges, OR-logic).
|
|
22
|
+
|
|
23
|
+
No organization, project, or credentials are built in — everything is
|
|
24
|
+
supplied via environment variables at runtime.
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
Three environment variables are required:
|
|
29
|
+
|
|
30
|
+
| Variable | Description |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `AZURE_DEVOPS_ORG` | Your Azure DevOps organization name (the part after `dev.azure.com/` in your URL) |
|
|
33
|
+
| `AZURE_DEVOPS_PROJECT` | The project name within that organization |
|
|
34
|
+
| `AZURE_DEVOPS_PAT` | A Personal Access Token with **Work Items: Read & Write** scope, created at `https://dev.azure.com/<your-org>/_usersSettings/tokens` |
|
|
35
|
+
|
|
36
|
+
## Usage with Claude Desktop (or any MCP client using `uvx`)
|
|
37
|
+
|
|
38
|
+
Add this to your MCP client's config (for Claude Desktop:
|
|
39
|
+
`claude_desktop_config.json`):
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"azure-devops": {
|
|
45
|
+
"command": "uvx",
|
|
46
|
+
"args": ["azure-devops-work-items-mcp"],
|
|
47
|
+
"env": {
|
|
48
|
+
"AZURE_DEVOPS_ORG": "your-org-name",
|
|
49
|
+
"AZURE_DEVOPS_PROJECT": "your-project-name",
|
|
50
|
+
"AZURE_DEVOPS_PAT": "your-personal-access-token"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Use the **full absolute path** to `uvx` (find it with `which uvx` on
|
|
58
|
+
macOS/Linux or `where.exe uvx` on Windows) if your client can't find it on
|
|
59
|
+
PATH — this is a known issue with some desktop apps not inheriting a full
|
|
60
|
+
shell PATH.
|
|
61
|
+
|
|
62
|
+
To pin to a specific version instead of always pulling the latest:
|
|
63
|
+
`"args": ["azure-devops-work-items-mcp==1.0.0"]`.
|
|
64
|
+
|
|
65
|
+
## Tools
|
|
66
|
+
|
|
67
|
+
- `ado_get_work_item(work_item_id)`
|
|
68
|
+
- `ado_list_work_items(state?, work_item_type?, assigned_to?, area_path?, custom_filter?, top?)`
|
|
69
|
+
- `ado_create_work_item(work_item_type, title, description?, assigned_to?, area_path?, iteration_path?, tags?)`
|
|
70
|
+
- `ado_update_work_item(work_item_id, title?, state?, description?, assigned_to?, area_path?, iteration_path?, tags?)`
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# azure-devops-work-items-mcp
|
|
2
|
+
|
|
3
|
+
An MCP server for Azure DevOps work items: get, list/filter, create, and
|
|
4
|
+
update. Filtering supports state, work item type, assignee, and area path
|
|
5
|
+
directly, plus an open-ended `custom_filter` parameter that accepts a raw
|
|
6
|
+
WIQL WHERE-clause fragment for anything else (sprint, priority, tags, date
|
|
7
|
+
ranges, OR-logic).
|
|
8
|
+
|
|
9
|
+
No organization, project, or credentials are built in — everything is
|
|
10
|
+
supplied via environment variables at runtime.
|
|
11
|
+
|
|
12
|
+
## Configuration
|
|
13
|
+
|
|
14
|
+
Three environment variables are required:
|
|
15
|
+
|
|
16
|
+
| Variable | Description |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `AZURE_DEVOPS_ORG` | Your Azure DevOps organization name (the part after `dev.azure.com/` in your URL) |
|
|
19
|
+
| `AZURE_DEVOPS_PROJECT` | The project name within that organization |
|
|
20
|
+
| `AZURE_DEVOPS_PAT` | A Personal Access Token with **Work Items: Read & Write** scope, created at `https://dev.azure.com/<your-org>/_usersSettings/tokens` |
|
|
21
|
+
|
|
22
|
+
## Usage with Claude Desktop (or any MCP client using `uvx`)
|
|
23
|
+
|
|
24
|
+
Add this to your MCP client's config (for Claude Desktop:
|
|
25
|
+
`claude_desktop_config.json`):
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"azure-devops": {
|
|
31
|
+
"command": "uvx",
|
|
32
|
+
"args": ["azure-devops-work-items-mcp"],
|
|
33
|
+
"env": {
|
|
34
|
+
"AZURE_DEVOPS_ORG": "your-org-name",
|
|
35
|
+
"AZURE_DEVOPS_PROJECT": "your-project-name",
|
|
36
|
+
"AZURE_DEVOPS_PAT": "your-personal-access-token"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Use the **full absolute path** to `uvx` (find it with `which uvx` on
|
|
44
|
+
macOS/Linux or `where.exe uvx` on Windows) if your client can't find it on
|
|
45
|
+
PATH — this is a known issue with some desktop apps not inheriting a full
|
|
46
|
+
shell PATH.
|
|
47
|
+
|
|
48
|
+
To pin to a specific version instead of always pulling the latest:
|
|
49
|
+
`"args": ["azure-devops-work-items-mcp==1.0.0"]`.
|
|
50
|
+
|
|
51
|
+
## Tools
|
|
52
|
+
|
|
53
|
+
- `ado_get_work_item(work_item_id)`
|
|
54
|
+
- `ado_list_work_items(state?, work_item_type?, assigned_to?, area_path?, custom_filter?, top?)`
|
|
55
|
+
- `ado_create_work_item(work_item_type, title, description?, assigned_to?, area_path?, iteration_path?, tags?)`
|
|
56
|
+
- `ado_update_work_item(work_item_id, title?, state?, description?, assigned_to?, area_path?, iteration_path?, tags?)`
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "azure-devops-work-items-mcp"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "MCP server for Azure DevOps work items — read, list, create, and update, with WIQL-based filtering"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Camylla Ribeiro" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["mcp", "azure-devops", "work-items", "model-context-protocol"]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"mcp>=1.2.0",
|
|
18
|
+
"requests>=2.31.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
azure-devops-work-items-mcp = "azure_devops_work_items_mcp.server:main"
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Repository = "https://github.com/Camy-Ribeiro/azure-devops-work-items-mcp"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/azure_devops_work_items_mcp"]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thin wrapper around the Azure DevOps REST API (Work Items only).
|
|
3
|
+
|
|
4
|
+
Auth: Personal Access Token via HTTP Basic (empty username, PAT as password).
|
|
5
|
+
Docs: https://learn.microsoft.com/en-us/rest/api/azure/devops/wit/
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
API_VERSION = "7.1"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _raise_with_body(resp: requests.Response) -> None:
|
|
17
|
+
"""Like resp.raise_for_status(), but includes the response body in the
|
|
18
|
+
error message — Azure DevOps puts the actual reason (e.g. bad WIQL,
|
|
19
|
+
invalid field, permission issue) in the JSON body, which plain
|
|
20
|
+
raise_for_status() discards."""
|
|
21
|
+
if resp.ok:
|
|
22
|
+
return
|
|
23
|
+
try:
|
|
24
|
+
detail = resp.json()
|
|
25
|
+
except ValueError:
|
|
26
|
+
detail = resp.text
|
|
27
|
+
raise requests.HTTPError(
|
|
28
|
+
f"{resp.status_code} {resp.reason} for url {resp.url}\nBody: {detail}",
|
|
29
|
+
response=resp,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AdoClient:
|
|
34
|
+
def __init__(self, organization: str, project: str, pat: str):
|
|
35
|
+
self.organization = organization
|
|
36
|
+
self.project = project
|
|
37
|
+
self.base_url = f"https://dev.azure.com/{organization}/{project}/_apis"
|
|
38
|
+
|
|
39
|
+
token = base64.b64encode(f":{pat}".encode()).decode()
|
|
40
|
+
self.headers = {
|
|
41
|
+
"Authorization": f"Basic {token}",
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
}
|
|
44
|
+
self.patch_headers = {
|
|
45
|
+
"Authorization": f"Basic {token}",
|
|
46
|
+
"Content-Type": "application/json-patch+json",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def get_work_item(self, work_item_id: int, expand: str = "all") -> dict:
|
|
50
|
+
url = f"{self.base_url}/wit/workitems/{work_item_id}"
|
|
51
|
+
params = {"api-version": API_VERSION, "$expand": expand}
|
|
52
|
+
resp = requests.get(url, headers=self.headers, params=params, timeout=30)
|
|
53
|
+
_raise_with_body(resp)
|
|
54
|
+
return resp.json()
|
|
55
|
+
|
|
56
|
+
def query_wiql(self, wiql: str) -> list:
|
|
57
|
+
url = f"{self.base_url}/wit/wiql"
|
|
58
|
+
params = {"api-version": API_VERSION}
|
|
59
|
+
resp = requests.post(
|
|
60
|
+
url, headers=self.headers, params=params, json={"query": wiql}, timeout=30
|
|
61
|
+
)
|
|
62
|
+
_raise_with_body(resp)
|
|
63
|
+
data = resp.json()
|
|
64
|
+
return [item["id"] for item in data.get("workItems", [])]
|
|
65
|
+
|
|
66
|
+
def get_work_items_batch(self, ids: list, fields: Optional[list] = None) -> list:
|
|
67
|
+
if not ids:
|
|
68
|
+
return []
|
|
69
|
+
url = f"{self.base_url}/wit/workitems"
|
|
70
|
+
params = {"api-version": API_VERSION, "ids": ",".join(str(i) for i in ids)}
|
|
71
|
+
if fields:
|
|
72
|
+
params["fields"] = ",".join(fields)
|
|
73
|
+
resp = requests.get(url, headers=self.headers, params=params, timeout=30)
|
|
74
|
+
_raise_with_body(resp)
|
|
75
|
+
return resp.json().get("value", [])
|
|
76
|
+
|
|
77
|
+
def create_work_item(self, work_item_type: str, patch_document: list) -> dict:
|
|
78
|
+
url = f"{self.base_url}/wit/workitems/${work_item_type}"
|
|
79
|
+
params = {"api-version": API_VERSION}
|
|
80
|
+
resp = requests.post(
|
|
81
|
+
url, headers=self.patch_headers, params=params, json=patch_document, timeout=30
|
|
82
|
+
)
|
|
83
|
+
_raise_with_body(resp)
|
|
84
|
+
return resp.json()
|
|
85
|
+
|
|
86
|
+
def update_work_item(self, work_item_id: int, patch_document: list) -> dict:
|
|
87
|
+
url = f"{self.base_url}/wit/workitems/{work_item_id}"
|
|
88
|
+
params = {"api-version": API_VERSION}
|
|
89
|
+
resp = requests.patch(
|
|
90
|
+
url, headers=self.patch_headers, params=params, json=patch_document, timeout=30
|
|
91
|
+
)
|
|
92
|
+
_raise_with_body(resp)
|
|
93
|
+
return resp.json()
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Azure DevOps MCP server — work items only (create, read, update, list).
|
|
3
|
+
|
|
4
|
+
Fully generic: organization, project, and PAT are all supplied via
|
|
5
|
+
environment variables (AZURE_DEVOPS_ORG, AZURE_DEVOPS_PROJECT,
|
|
6
|
+
AZURE_DEVOPS_PAT). None of them are hardcoded or defaulted, so this
|
|
7
|
+
package carries no reference to any specific organization or project.
|
|
8
|
+
|
|
9
|
+
NOTE: all @mcp.tool() definitions live above the __main__ block on purpose —
|
|
10
|
+
if a tool is defined after mcp.run() is called, FastMCP never registers it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from mcp.server.fastmcp import FastMCP
|
|
17
|
+
|
|
18
|
+
from .ado_client import AdoClient
|
|
19
|
+
|
|
20
|
+
REQUIRED_ENV_VARS = ("AZURE_DEVOPS_ORG", "AZURE_DEVOPS_PROJECT", "AZURE_DEVOPS_PAT")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _load_config() -> tuple[str, str, str]:
|
|
24
|
+
values = {name: os.environ.get(name, "") for name in REQUIRED_ENV_VARS}
|
|
25
|
+
missing = [name for name, value in values.items() if not value]
|
|
26
|
+
if missing:
|
|
27
|
+
raise RuntimeError(
|
|
28
|
+
"Missing required environment variable(s): "
|
|
29
|
+
f"{', '.join(missing)}. Set AZURE_DEVOPS_ORG (e.g. 'my-org'), "
|
|
30
|
+
"AZURE_DEVOPS_PROJECT (e.g. 'my-project'), and AZURE_DEVOPS_PAT "
|
|
31
|
+
"(a Personal Access Token with 'Work Items: Read & Write' scope) "
|
|
32
|
+
"in the env block of your MCP client's config."
|
|
33
|
+
)
|
|
34
|
+
return values["AZURE_DEVOPS_ORG"], values["AZURE_DEVOPS_PROJECT"], values["AZURE_DEVOPS_PAT"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
ORGANIZATION, PROJECT, PAT = _load_config()
|
|
38
|
+
client = AdoClient(organization=ORGANIZATION, project=PROJECT, pat=PAT)
|
|
39
|
+
|
|
40
|
+
mcp = FastMCP("azure-devops-work-items")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _simplify(work_item: dict) -> dict:
|
|
44
|
+
fields = work_item.get("fields", {})
|
|
45
|
+
assigned_to = fields.get("System.AssignedTo") or {}
|
|
46
|
+
return {
|
|
47
|
+
"id": work_item.get("id"),
|
|
48
|
+
"url": (work_item.get("_links", {}).get("html", {}) or {}).get("href"),
|
|
49
|
+
"title": fields.get("System.Title"),
|
|
50
|
+
"type": fields.get("System.WorkItemType"),
|
|
51
|
+
"state": fields.get("System.State"),
|
|
52
|
+
"assigned_to": assigned_to.get("displayName"),
|
|
53
|
+
"area_path": fields.get("System.AreaPath"),
|
|
54
|
+
"iteration_path": fields.get("System.IterationPath"),
|
|
55
|
+
"tags": fields.get("System.Tags"),
|
|
56
|
+
"description": fields.get("System.Description"),
|
|
57
|
+
"created_date": fields.get("System.CreatedDate"),
|
|
58
|
+
"changed_date": fields.get("System.ChangedDate"),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _wiql_escape(value: str) -> str:
|
|
63
|
+
"""Escape single quotes for safe interpolation into a WIQL string literal."""
|
|
64
|
+
return value.replace("'", "''")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@mcp.tool()
|
|
68
|
+
def ado_get_work_item(work_item_id: int) -> dict:
|
|
69
|
+
"""Get a single Azure DevOps work item by its numeric ID."""
|
|
70
|
+
item = client.get_work_item(work_item_id)
|
|
71
|
+
return _simplify(item)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mcp.tool()
|
|
75
|
+
def ado_list_work_items(
|
|
76
|
+
state: Optional[str] = None,
|
|
77
|
+
work_item_type: Optional[str] = None,
|
|
78
|
+
assigned_to: Optional[str] = None,
|
|
79
|
+
area_path: Optional[str] = None,
|
|
80
|
+
custom_filter: Optional[str] = None,
|
|
81
|
+
top: int = 50,
|
|
82
|
+
) -> list:
|
|
83
|
+
"""
|
|
84
|
+
List work items in the configured project (set via AZURE_DEVOPS_PROJECT),
|
|
85
|
+
optionally filtered by state (e.g. 'Active', 'New', 'Closed'), work
|
|
86
|
+
item type (e.g. 'Bug', 'Task', 'User Story', 'Epic'), assignee (display
|
|
87
|
+
name or email), or area_path (e.g. 'MyProject\\SubArea' — matches that
|
|
88
|
+
area and any sub-areas under it). Returns up to `top` items, most
|
|
89
|
+
recently changed first.
|
|
90
|
+
|
|
91
|
+
For anything not covered by the parameters above (sprint/iteration,
|
|
92
|
+
tags, priority, custom fields, date ranges, OR-logic, etc.), pass a raw
|
|
93
|
+
WIQL WHERE-clause fragment via `custom_filter` — it is ANDed together
|
|
94
|
+
with the other filters. Reference fields by their WIQL name in square
|
|
95
|
+
brackets. Examples:
|
|
96
|
+
- Sprint: "[System.IterationPath] UNDER 'MyProject\\\\Sprint 24'"
|
|
97
|
+
- Priority: "[Microsoft.VSTS.Common.Priority] = 1"
|
|
98
|
+
- Tags: "[System.Tags] CONTAINS 'urgent'"
|
|
99
|
+
- Date range: "[System.CreatedDate] >= '2026-06-01'"
|
|
100
|
+
- OR-logic: "([System.Tags] CONTAINS 'urgent' OR [Microsoft.VSTS.Common.Priority] = 1)"
|
|
101
|
+
String literals inside custom_filter must use single quotes; escape a
|
|
102
|
+
literal single quote by doubling it ('').
|
|
103
|
+
"""
|
|
104
|
+
conditions = [f"[System.TeamProject] = '{_wiql_escape(PROJECT)}'"]
|
|
105
|
+
if state:
|
|
106
|
+
conditions.append(f"[System.State] = '{_wiql_escape(state)}'")
|
|
107
|
+
if work_item_type:
|
|
108
|
+
conditions.append(f"[System.WorkItemType] = '{_wiql_escape(work_item_type)}'")
|
|
109
|
+
if assigned_to:
|
|
110
|
+
conditions.append(f"[System.AssignedTo] = '{_wiql_escape(assigned_to)}'")
|
|
111
|
+
if area_path:
|
|
112
|
+
conditions.append(f"[System.AreaPath] UNDER '{_wiql_escape(area_path)}'")
|
|
113
|
+
if custom_filter:
|
|
114
|
+
conditions.append(f"({custom_filter})")
|
|
115
|
+
|
|
116
|
+
wiql = (
|
|
117
|
+
"SELECT [System.Id] FROM WorkItems "
|
|
118
|
+
f"WHERE {' AND '.join(conditions)} "
|
|
119
|
+
"ORDER BY [System.ChangedDate] DESC"
|
|
120
|
+
)
|
|
121
|
+
ids = client.query_wiql(wiql)[:top]
|
|
122
|
+
items = client.get_work_items_batch(ids)
|
|
123
|
+
return [_simplify(item) for item in items]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@mcp.tool()
|
|
127
|
+
def ado_create_work_item(
|
|
128
|
+
work_item_type: str,
|
|
129
|
+
title: str,
|
|
130
|
+
description: Optional[str] = None,
|
|
131
|
+
assigned_to: Optional[str] = None,
|
|
132
|
+
area_path: Optional[str] = None,
|
|
133
|
+
iteration_path: Optional[str] = None,
|
|
134
|
+
tags: Optional[str] = None,
|
|
135
|
+
) -> dict:
|
|
136
|
+
"""
|
|
137
|
+
Create a new Azure DevOps work item in the configured project.
|
|
138
|
+
work_item_type is e.g. 'Bug', 'Task', 'User Story', 'Epic'.
|
|
139
|
+
area_path/iteration_path default to the project root if omitted.
|
|
140
|
+
tags is a semicolon-separated string, e.g. 'urgent;client-facing'.
|
|
141
|
+
"""
|
|
142
|
+
patch = [{"op": "add", "path": "/fields/System.Title", "value": title}]
|
|
143
|
+
if description:
|
|
144
|
+
patch.append({"op": "add", "path": "/fields/System.Description", "value": description})
|
|
145
|
+
if assigned_to:
|
|
146
|
+
patch.append({"op": "add", "path": "/fields/System.AssignedTo", "value": assigned_to})
|
|
147
|
+
if area_path:
|
|
148
|
+
patch.append({"op": "add", "path": "/fields/System.AreaPath", "value": area_path})
|
|
149
|
+
if iteration_path:
|
|
150
|
+
patch.append(
|
|
151
|
+
{"op": "add", "path": "/fields/System.IterationPath", "value": iteration_path}
|
|
152
|
+
)
|
|
153
|
+
if tags:
|
|
154
|
+
patch.append({"op": "add", "path": "/fields/System.Tags", "value": tags})
|
|
155
|
+
|
|
156
|
+
item = client.create_work_item(work_item_type, patch)
|
|
157
|
+
return _simplify(item)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@mcp.tool()
|
|
161
|
+
def ado_update_work_item(
|
|
162
|
+
work_item_id: int,
|
|
163
|
+
title: Optional[str] = None,
|
|
164
|
+
state: Optional[str] = None,
|
|
165
|
+
description: Optional[str] = None,
|
|
166
|
+
assigned_to: Optional[str] = None,
|
|
167
|
+
area_path: Optional[str] = None,
|
|
168
|
+
iteration_path: Optional[str] = None,
|
|
169
|
+
tags: Optional[str] = None,
|
|
170
|
+
) -> dict:
|
|
171
|
+
"""
|
|
172
|
+
Update fields on an existing Azure DevOps work item. Only fields you
|
|
173
|
+
provide are changed; everything else is left as-is.
|
|
174
|
+
"""
|
|
175
|
+
field_map = {
|
|
176
|
+
"System.Title": title,
|
|
177
|
+
"System.State": state,
|
|
178
|
+
"System.Description": description,
|
|
179
|
+
"System.AssignedTo": assigned_to,
|
|
180
|
+
"System.AreaPath": area_path,
|
|
181
|
+
"System.IterationPath": iteration_path,
|
|
182
|
+
"System.Tags": tags,
|
|
183
|
+
}
|
|
184
|
+
patch = [
|
|
185
|
+
{"op": "add", "path": f"/fields/{field}", "value": value}
|
|
186
|
+
for field, value in field_map.items()
|
|
187
|
+
if value is not None
|
|
188
|
+
]
|
|
189
|
+
if not patch:
|
|
190
|
+
raise ValueError("Provide at least one field to update.")
|
|
191
|
+
|
|
192
|
+
item = client.update_work_item(work_item_id, patch)
|
|
193
|
+
return _simplify(item)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def main() -> None:
|
|
197
|
+
mcp.run()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
main()
|