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.
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .DS_Store
8
+ uv.lock
@@ -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,3 @@
1
+ """Azure DevOps work items MCP server."""
2
+
3
+ __version__ = "1.0.0"
@@ -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()