mcp-ticketer 0.4.11__py3-none-any.whl → 0.12.0__py3-none-any.whl
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.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +9 -3
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1308 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +334 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +313 -96
- mcp_ticketer/adapters/jira.py +251 -1
- mcp_ticketer/adapters/linear/adapter.py +524 -22
- mcp_ticketer/adapters/linear/client.py +61 -9
- mcp_ticketer/adapters/linear/mappers.py +9 -3
- mcp_ticketer/cache/memory.py +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +1 -1
- mcp_ticketer/cli/codex_configure.py +80 -1
- mcp_ticketer/cli/configure.py +33 -43
- mcp_ticketer/cli/diagnostics.py +18 -16
- mcp_ticketer/cli/discover.py +288 -21
- mcp_ticketer/cli/gemini_configure.py +1 -1
- mcp_ticketer/cli/instruction_commands.py +429 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +1199 -227
- mcp_ticketer/cli/mcp_configure.py +1 -1
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +412 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/simple_health.py +1 -1
- mcp_ticketer/cli/ticket_commands.py +14 -13
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +12 -0
- mcp_ticketer/core/adapter.py +4 -4
- mcp_ticketer/core/config.py +17 -10
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +3 -3
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/mappers.py +1 -1
- mcp_ticketer/core/models.py +1 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +17 -1
- mcp_ticketer/core/registry.py +1 -1
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/main.py +82 -69
- mcp_ticketer/mcp/server/tools/__init__.py +9 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +63 -16
- mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +154 -5
- mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +157 -4
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +14 -12
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +106 -52
- mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
mcp_ticketer/adapters/jira.py
CHANGED
|
@@ -13,7 +13,15 @@ from httpx import AsyncClient, HTTPStatusError, TimeoutException
|
|
|
13
13
|
|
|
14
14
|
from ..core.adapter import BaseAdapter
|
|
15
15
|
from ..core.env_loader import load_adapter_config, validate_adapter_config
|
|
16
|
-
from ..core.models import
|
|
16
|
+
from ..core.models import (
|
|
17
|
+
Attachment,
|
|
18
|
+
Comment,
|
|
19
|
+
Epic,
|
|
20
|
+
Priority,
|
|
21
|
+
SearchQuery,
|
|
22
|
+
Task,
|
|
23
|
+
TicketState,
|
|
24
|
+
)
|
|
17
25
|
from ..core.registry import AdapterRegistry
|
|
18
26
|
|
|
19
27
|
logger = logging.getLogger(__name__)
|
|
@@ -995,6 +1003,248 @@ class JiraAdapter(BaseAdapter[Union[Epic, Task]]):
|
|
|
995
1003
|
except Exception:
|
|
996
1004
|
return None
|
|
997
1005
|
|
|
1006
|
+
async def list_labels(self) -> builtins.list[dict[str, Any]]:
|
|
1007
|
+
"""List all labels used in the project.
|
|
1008
|
+
|
|
1009
|
+
JIRA doesn't have a direct "list all labels" endpoint, so we query
|
|
1010
|
+
recent issues and extract unique labels from them.
|
|
1011
|
+
|
|
1012
|
+
Returns:
|
|
1013
|
+
List of label dictionaries with 'id' and 'name' fields
|
|
1014
|
+
|
|
1015
|
+
"""
|
|
1016
|
+
try:
|
|
1017
|
+
# Query recent issues to get labels in use
|
|
1018
|
+
jql = f"project = {self.project_key} ORDER BY updated DESC"
|
|
1019
|
+
data = await self._make_request(
|
|
1020
|
+
"GET",
|
|
1021
|
+
"search/jql",
|
|
1022
|
+
params={
|
|
1023
|
+
"jql": jql,
|
|
1024
|
+
"maxResults": 100, # Sample from recent 100 issues
|
|
1025
|
+
"fields": "labels",
|
|
1026
|
+
},
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
# Collect unique labels
|
|
1030
|
+
unique_labels = set()
|
|
1031
|
+
for issue in data.get("issues", []):
|
|
1032
|
+
labels = issue.get("fields", {}).get("labels", [])
|
|
1033
|
+
for label in labels:
|
|
1034
|
+
if isinstance(label, dict):
|
|
1035
|
+
unique_labels.add(label.get("name", ""))
|
|
1036
|
+
else:
|
|
1037
|
+
unique_labels.add(str(label))
|
|
1038
|
+
|
|
1039
|
+
# Transform to standardized format
|
|
1040
|
+
return [
|
|
1041
|
+
{"id": label, "name": label} for label in sorted(unique_labels) if label
|
|
1042
|
+
]
|
|
1043
|
+
|
|
1044
|
+
except Exception:
|
|
1045
|
+
# Fallback: return empty list if query fails
|
|
1046
|
+
return []
|
|
1047
|
+
|
|
1048
|
+
async def update_epic(self, epic_id: str, updates: dict[str, Any]) -> Epic | None:
|
|
1049
|
+
"""Update a JIRA Epic with epic-specific field handling.
|
|
1050
|
+
|
|
1051
|
+
Args:
|
|
1052
|
+
epic_id: Epic identifier (key like PROJ-123 or ID)
|
|
1053
|
+
updates: Dictionary with fields to update:
|
|
1054
|
+
- title: Epic title (maps to summary)
|
|
1055
|
+
- description: Epic description (auto-converted to ADF)
|
|
1056
|
+
- state: TicketState value (transitions via workflow)
|
|
1057
|
+
- tags: List of labels
|
|
1058
|
+
- priority: Priority level
|
|
1059
|
+
|
|
1060
|
+
Returns:
|
|
1061
|
+
Updated Epic object or None if not found
|
|
1062
|
+
|
|
1063
|
+
Raises:
|
|
1064
|
+
ValueError: If no fields provided for update
|
|
1065
|
+
HTTPStatusError: If update fails
|
|
1066
|
+
|
|
1067
|
+
"""
|
|
1068
|
+
fields = {}
|
|
1069
|
+
|
|
1070
|
+
# Map title to summary
|
|
1071
|
+
if "title" in updates:
|
|
1072
|
+
fields["summary"] = updates["title"]
|
|
1073
|
+
|
|
1074
|
+
# Convert description to ADF format
|
|
1075
|
+
if "description" in updates:
|
|
1076
|
+
fields["description"] = self._convert_to_adf(updates["description"])
|
|
1077
|
+
|
|
1078
|
+
# Map tags to labels
|
|
1079
|
+
if "tags" in updates:
|
|
1080
|
+
fields["labels"] = updates["tags"]
|
|
1081
|
+
|
|
1082
|
+
# Map priority (some JIRA configs allow priority on Epics)
|
|
1083
|
+
if "priority" in updates:
|
|
1084
|
+
priority_value = updates["priority"]
|
|
1085
|
+
if isinstance(priority_value, Priority):
|
|
1086
|
+
fields["priority"] = {
|
|
1087
|
+
"name": self._map_priority_to_jira(priority_value)
|
|
1088
|
+
}
|
|
1089
|
+
else:
|
|
1090
|
+
# String priority passed directly
|
|
1091
|
+
fields["priority"] = {"name": priority_value}
|
|
1092
|
+
|
|
1093
|
+
if not fields and "state" not in updates:
|
|
1094
|
+
raise ValueError("At least one field must be updated")
|
|
1095
|
+
|
|
1096
|
+
# Apply field updates if any
|
|
1097
|
+
if fields:
|
|
1098
|
+
await self._make_request("PUT", f"issue/{epic_id}", data={"fields": fields})
|
|
1099
|
+
|
|
1100
|
+
# Handle state transitions separately (JIRA uses workflow transitions)
|
|
1101
|
+
if "state" in updates:
|
|
1102
|
+
await self.transition_state(epic_id, updates["state"])
|
|
1103
|
+
|
|
1104
|
+
# Fetch and return updated epic
|
|
1105
|
+
return await self.read(epic_id)
|
|
1106
|
+
|
|
1107
|
+
async def add_attachment(
|
|
1108
|
+
self, ticket_id: str, file_path: str, description: str | None = None
|
|
1109
|
+
) -> Attachment:
|
|
1110
|
+
"""Attach file to JIRA issue (including Epic).
|
|
1111
|
+
|
|
1112
|
+
Args:
|
|
1113
|
+
ticket_id: Issue key (e.g., PROJ-123) or ID
|
|
1114
|
+
file_path: Path to file to attach
|
|
1115
|
+
description: Optional description (stored in metadata, not used by JIRA directly)
|
|
1116
|
+
|
|
1117
|
+
Returns:
|
|
1118
|
+
Attachment object with metadata
|
|
1119
|
+
|
|
1120
|
+
Raises:
|
|
1121
|
+
FileNotFoundError: If file doesn't exist
|
|
1122
|
+
ValueError: If credentials invalid
|
|
1123
|
+
HTTPStatusError: If upload fails
|
|
1124
|
+
|
|
1125
|
+
"""
|
|
1126
|
+
from pathlib import Path
|
|
1127
|
+
|
|
1128
|
+
# Validate credentials before attempting operation
|
|
1129
|
+
is_valid, error_message = self.validate_credentials()
|
|
1130
|
+
if not is_valid:
|
|
1131
|
+
raise ValueError(error_message)
|
|
1132
|
+
|
|
1133
|
+
file_path_obj = Path(file_path)
|
|
1134
|
+
if not file_path_obj.exists():
|
|
1135
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
1136
|
+
|
|
1137
|
+
# JIRA requires special header for attachment upload
|
|
1138
|
+
headers = {
|
|
1139
|
+
"X-Atlassian-Token": "no-check",
|
|
1140
|
+
# Don't set Content-Type - let httpx handle multipart
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
# Prepare multipart file upload
|
|
1144
|
+
with open(file_path_obj, "rb") as f:
|
|
1145
|
+
files = {"file": (file_path_obj.name, f, "application/octet-stream")}
|
|
1146
|
+
|
|
1147
|
+
url = f"{self.api_base}/issue/{ticket_id}/attachments"
|
|
1148
|
+
|
|
1149
|
+
# Use existing client infrastructure
|
|
1150
|
+
async with await self._get_client() as client:
|
|
1151
|
+
response = await client.post(
|
|
1152
|
+
url, files=files, headers={**self.headers, **headers}
|
|
1153
|
+
)
|
|
1154
|
+
response.raise_for_status()
|
|
1155
|
+
|
|
1156
|
+
# JIRA returns array with single attachment
|
|
1157
|
+
attachment_data = response.json()[0]
|
|
1158
|
+
|
|
1159
|
+
return Attachment(
|
|
1160
|
+
id=attachment_data["id"],
|
|
1161
|
+
ticket_id=ticket_id,
|
|
1162
|
+
filename=attachment_data["filename"],
|
|
1163
|
+
url=attachment_data["content"],
|
|
1164
|
+
content_type=attachment_data["mimeType"],
|
|
1165
|
+
size_bytes=attachment_data["size"],
|
|
1166
|
+
created_at=parse_jira_datetime(attachment_data["created"]),
|
|
1167
|
+
created_by=attachment_data["author"]["displayName"],
|
|
1168
|
+
description=description,
|
|
1169
|
+
metadata={"jira": attachment_data},
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
async def get_attachments(self, ticket_id: str) -> builtins.list[Attachment]:
|
|
1173
|
+
"""Get all attachments for a JIRA issue.
|
|
1174
|
+
|
|
1175
|
+
Args:
|
|
1176
|
+
ticket_id: Issue key or ID
|
|
1177
|
+
|
|
1178
|
+
Returns:
|
|
1179
|
+
List of Attachment objects
|
|
1180
|
+
|
|
1181
|
+
Raises:
|
|
1182
|
+
ValueError: If credentials invalid
|
|
1183
|
+
HTTPStatusError: If request fails
|
|
1184
|
+
|
|
1185
|
+
"""
|
|
1186
|
+
# Validate credentials before attempting operation
|
|
1187
|
+
is_valid, error_message = self.validate_credentials()
|
|
1188
|
+
if not is_valid:
|
|
1189
|
+
raise ValueError(error_message)
|
|
1190
|
+
|
|
1191
|
+
# Fetch issue with attachment field
|
|
1192
|
+
issue = await self._make_request(
|
|
1193
|
+
"GET", f"issue/{ticket_id}", params={"fields": "attachment"}
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
attachments = []
|
|
1197
|
+
for att_data in issue.get("fields", {}).get("attachment", []):
|
|
1198
|
+
attachments.append(
|
|
1199
|
+
Attachment(
|
|
1200
|
+
id=att_data["id"],
|
|
1201
|
+
ticket_id=ticket_id,
|
|
1202
|
+
filename=att_data["filename"],
|
|
1203
|
+
url=att_data["content"],
|
|
1204
|
+
content_type=att_data["mimeType"],
|
|
1205
|
+
size_bytes=att_data["size"],
|
|
1206
|
+
created_at=parse_jira_datetime(att_data["created"]),
|
|
1207
|
+
created_by=att_data["author"]["displayName"],
|
|
1208
|
+
metadata={"jira": att_data},
|
|
1209
|
+
)
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
return attachments
|
|
1213
|
+
|
|
1214
|
+
async def delete_attachment(self, ticket_id: str, attachment_id: str) -> bool:
|
|
1215
|
+
"""Delete an attachment from a JIRA issue.
|
|
1216
|
+
|
|
1217
|
+
Args:
|
|
1218
|
+
ticket_id: Issue key or ID (for validation/context)
|
|
1219
|
+
attachment_id: Attachment ID to delete
|
|
1220
|
+
|
|
1221
|
+
Returns:
|
|
1222
|
+
True if deleted successfully, False otherwise
|
|
1223
|
+
|
|
1224
|
+
Raises:
|
|
1225
|
+
ValueError: If credentials invalid
|
|
1226
|
+
|
|
1227
|
+
"""
|
|
1228
|
+
# Validate credentials before attempting operation
|
|
1229
|
+
is_valid, error_message = self.validate_credentials()
|
|
1230
|
+
if not is_valid:
|
|
1231
|
+
raise ValueError(error_message)
|
|
1232
|
+
|
|
1233
|
+
try:
|
|
1234
|
+
await self._make_request("DELETE", f"attachment/{attachment_id}")
|
|
1235
|
+
return True
|
|
1236
|
+
except HTTPStatusError as e:
|
|
1237
|
+
if e.response.status_code == 404:
|
|
1238
|
+
logger.warning(f"Attachment {attachment_id} not found")
|
|
1239
|
+
return False
|
|
1240
|
+
logger.error(
|
|
1241
|
+
f"Failed to delete attachment {attachment_id}: {e.response.status_code} - {e.response.text}"
|
|
1242
|
+
)
|
|
1243
|
+
return False
|
|
1244
|
+
except Exception as e:
|
|
1245
|
+
logger.error(f"Unexpected error deleting attachment {attachment_id}: {e}")
|
|
1246
|
+
return False
|
|
1247
|
+
|
|
998
1248
|
async def close(self) -> None:
|
|
999
1249
|
"""Close the adapter and cleanup resources."""
|
|
1000
1250
|
# Clear caches
|