mcp-ticketer 0.1.8__py3-none-any.whl → 0.1.11__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 +1 -1
- mcp_ticketer/adapters/github.py +263 -0
- mcp_ticketer/adapters/linear.py +220 -0
- mcp_ticketer/cli/main.py +26 -0
- mcp_ticketer/mcp/server.py +349 -15
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.11.dist-info}/METADATA +6 -5
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.11.dist-info}/RECORD +11 -11
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.11.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.11.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.11.dist-info}/top_level.txt +0 -0
mcp_ticketer/__version__.py
CHANGED
mcp_ticketer/adapters/github.py
CHANGED
|
@@ -965,6 +965,269 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
965
965
|
|
|
966
966
|
return response.status_code == 201
|
|
967
967
|
|
|
968
|
+
async def create_pull_request(
|
|
969
|
+
self,
|
|
970
|
+
ticket_id: str,
|
|
971
|
+
base_branch: str = "main",
|
|
972
|
+
head_branch: Optional[str] = None,
|
|
973
|
+
title: Optional[str] = None,
|
|
974
|
+
body: Optional[str] = None,
|
|
975
|
+
draft: bool = False,
|
|
976
|
+
) -> Dict[str, Any]:
|
|
977
|
+
"""Create a pull request linked to an issue.
|
|
978
|
+
|
|
979
|
+
Args:
|
|
980
|
+
ticket_id: Issue number to link the PR to
|
|
981
|
+
base_branch: Target branch for the PR (default: main)
|
|
982
|
+
head_branch: Source branch name (auto-generated if not provided)
|
|
983
|
+
title: PR title (uses ticket title if not provided)
|
|
984
|
+
body: PR description (auto-generated with issue link if not provided)
|
|
985
|
+
draft: Create as draft PR
|
|
986
|
+
|
|
987
|
+
Returns:
|
|
988
|
+
Dictionary with PR details including number, url, and branch
|
|
989
|
+
"""
|
|
990
|
+
try:
|
|
991
|
+
issue_number = int(ticket_id)
|
|
992
|
+
except ValueError:
|
|
993
|
+
raise ValueError(f"Invalid issue number: {ticket_id}")
|
|
994
|
+
|
|
995
|
+
# Get the issue details
|
|
996
|
+
issue = await self.read(ticket_id)
|
|
997
|
+
if not issue:
|
|
998
|
+
raise ValueError(f"Issue #{ticket_id} not found")
|
|
999
|
+
|
|
1000
|
+
# Auto-generate branch name if not provided
|
|
1001
|
+
if not head_branch:
|
|
1002
|
+
# Create branch name from issue number and title
|
|
1003
|
+
# e.g., "123-fix-authentication-bug"
|
|
1004
|
+
safe_title = "-".join(
|
|
1005
|
+
issue.title.lower()
|
|
1006
|
+
.replace("[", "")
|
|
1007
|
+
.replace("]", "")
|
|
1008
|
+
.replace("#", "")
|
|
1009
|
+
.replace("/", "-")
|
|
1010
|
+
.replace("\\", "-")
|
|
1011
|
+
.split()[:5] # Limit to 5 words
|
|
1012
|
+
)
|
|
1013
|
+
head_branch = f"{issue_number}-{safe_title}"
|
|
1014
|
+
|
|
1015
|
+
# Auto-generate title if not provided
|
|
1016
|
+
if not title:
|
|
1017
|
+
# Include issue number in PR title
|
|
1018
|
+
title = f"[#{issue_number}] {issue.title}"
|
|
1019
|
+
|
|
1020
|
+
# Auto-generate body if not provided
|
|
1021
|
+
if not body:
|
|
1022
|
+
body = f"""## Summary
|
|
1023
|
+
|
|
1024
|
+
This PR addresses issue #{issue_number}.
|
|
1025
|
+
|
|
1026
|
+
**Issue:** #{issue_number} - {issue.title}
|
|
1027
|
+
**Link:** {issue.metadata.get('github', {}).get('url', '')}
|
|
1028
|
+
|
|
1029
|
+
## Description
|
|
1030
|
+
|
|
1031
|
+
{issue.description or 'No description provided.'}
|
|
1032
|
+
|
|
1033
|
+
## Changes
|
|
1034
|
+
|
|
1035
|
+
- [ ] Implementation details to be added
|
|
1036
|
+
|
|
1037
|
+
## Testing
|
|
1038
|
+
|
|
1039
|
+
- [ ] Tests have been added/updated
|
|
1040
|
+
- [ ] All tests pass
|
|
1041
|
+
|
|
1042
|
+
## Checklist
|
|
1043
|
+
|
|
1044
|
+
- [ ] Code follows project style guidelines
|
|
1045
|
+
- [ ] Self-review completed
|
|
1046
|
+
- [ ] Documentation updated if needed
|
|
1047
|
+
|
|
1048
|
+
Fixes #{issue_number}
|
|
1049
|
+
"""
|
|
1050
|
+
|
|
1051
|
+
# Check if the head branch exists
|
|
1052
|
+
try:
|
|
1053
|
+
branch_response = await self.client.get(
|
|
1054
|
+
f"/repos/{self.owner}/{self.repo}/branches/{head_branch}"
|
|
1055
|
+
)
|
|
1056
|
+
branch_exists = branch_response.status_code == 200
|
|
1057
|
+
except httpx.HTTPError:
|
|
1058
|
+
branch_exists = False
|
|
1059
|
+
|
|
1060
|
+
if not branch_exists:
|
|
1061
|
+
# Get the base branch SHA
|
|
1062
|
+
base_response = await self.client.get(
|
|
1063
|
+
f"/repos/{self.owner}/{self.repo}/branches/{base_branch}"
|
|
1064
|
+
)
|
|
1065
|
+
base_response.raise_for_status()
|
|
1066
|
+
base_sha = base_response.json()["commit"]["sha"]
|
|
1067
|
+
|
|
1068
|
+
# Create the new branch
|
|
1069
|
+
ref_response = await self.client.post(
|
|
1070
|
+
f"/repos/{self.owner}/{self.repo}/git/refs",
|
|
1071
|
+
json={
|
|
1072
|
+
"ref": f"refs/heads/{head_branch}",
|
|
1073
|
+
"sha": base_sha,
|
|
1074
|
+
}
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
if ref_response.status_code != 201:
|
|
1078
|
+
# Branch might already exist on remote, try to use it
|
|
1079
|
+
pass
|
|
1080
|
+
|
|
1081
|
+
# Create the pull request
|
|
1082
|
+
pr_data = {
|
|
1083
|
+
"title": title,
|
|
1084
|
+
"body": body,
|
|
1085
|
+
"head": head_branch,
|
|
1086
|
+
"base": base_branch,
|
|
1087
|
+
"draft": draft,
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
pr_response = await self.client.post(
|
|
1091
|
+
f"/repos/{self.owner}/{self.repo}/pulls",
|
|
1092
|
+
json=pr_data
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
if pr_response.status_code == 422:
|
|
1096
|
+
# PR might already exist, try to get it
|
|
1097
|
+
search_response = await self.client.get(
|
|
1098
|
+
f"/repos/{self.owner}/{self.repo}/pulls",
|
|
1099
|
+
params={
|
|
1100
|
+
"head": f"{self.owner}:{head_branch}",
|
|
1101
|
+
"base": base_branch,
|
|
1102
|
+
"state": "open",
|
|
1103
|
+
}
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
if search_response.status_code == 200:
|
|
1107
|
+
existing_prs = search_response.json()
|
|
1108
|
+
if existing_prs:
|
|
1109
|
+
pr = existing_prs[0]
|
|
1110
|
+
return {
|
|
1111
|
+
"number": pr["number"],
|
|
1112
|
+
"url": pr["html_url"],
|
|
1113
|
+
"api_url": pr["url"],
|
|
1114
|
+
"branch": head_branch,
|
|
1115
|
+
"state": pr["state"],
|
|
1116
|
+
"draft": pr.get("draft", False),
|
|
1117
|
+
"title": pr["title"],
|
|
1118
|
+
"existing": True,
|
|
1119
|
+
"linked_issue": issue_number,
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
raise ValueError(f"Failed to create PR: {pr_response.text}")
|
|
1123
|
+
|
|
1124
|
+
pr_response.raise_for_status()
|
|
1125
|
+
pr = pr_response.json()
|
|
1126
|
+
|
|
1127
|
+
# Add a comment to the issue about the PR
|
|
1128
|
+
await self.add_comment(
|
|
1129
|
+
Comment(
|
|
1130
|
+
ticket_id=ticket_id,
|
|
1131
|
+
content=f"Pull request #{pr['number']} has been created: {pr['html_url']}",
|
|
1132
|
+
author="system",
|
|
1133
|
+
)
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
return {
|
|
1137
|
+
"number": pr["number"],
|
|
1138
|
+
"url": pr["html_url"],
|
|
1139
|
+
"api_url": pr["url"],
|
|
1140
|
+
"branch": head_branch,
|
|
1141
|
+
"state": pr["state"],
|
|
1142
|
+
"draft": pr.get("draft", False),
|
|
1143
|
+
"title": pr["title"],
|
|
1144
|
+
"linked_issue": issue_number,
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
async def link_existing_pull_request(
|
|
1148
|
+
self,
|
|
1149
|
+
ticket_id: str,
|
|
1150
|
+
pr_url: str,
|
|
1151
|
+
) -> Dict[str, Any]:
|
|
1152
|
+
"""Link an existing pull request to a ticket.
|
|
1153
|
+
|
|
1154
|
+
Args:
|
|
1155
|
+
ticket_id: Issue number to link the PR to
|
|
1156
|
+
pr_url: GitHub PR URL to link
|
|
1157
|
+
|
|
1158
|
+
Returns:
|
|
1159
|
+
Dictionary with link status and PR details
|
|
1160
|
+
"""
|
|
1161
|
+
try:
|
|
1162
|
+
issue_number = int(ticket_id)
|
|
1163
|
+
except ValueError:
|
|
1164
|
+
raise ValueError(f"Invalid issue number: {ticket_id}")
|
|
1165
|
+
|
|
1166
|
+
# Parse PR URL to extract owner, repo, and PR number
|
|
1167
|
+
# Expected format: https://github.com/owner/repo/pull/123
|
|
1168
|
+
import re
|
|
1169
|
+
pr_pattern = r"github\.com/([^/]+)/([^/]+)/pull/(\d+)"
|
|
1170
|
+
match = re.search(pr_pattern, pr_url)
|
|
1171
|
+
|
|
1172
|
+
if not match:
|
|
1173
|
+
raise ValueError(f"Invalid GitHub PR URL format: {pr_url}")
|
|
1174
|
+
|
|
1175
|
+
pr_owner, pr_repo, pr_number = match.groups()
|
|
1176
|
+
|
|
1177
|
+
# Verify the PR is from the same repository
|
|
1178
|
+
if pr_owner != self.owner or pr_repo != self.repo:
|
|
1179
|
+
raise ValueError(
|
|
1180
|
+
f"PR must be from the same repository ({self.owner}/{self.repo})"
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
# Get PR details
|
|
1184
|
+
pr_response = await self.client.get(
|
|
1185
|
+
f"/repos/{self.owner}/{self.repo}/pulls/{pr_number}"
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
if pr_response.status_code == 404:
|
|
1189
|
+
raise ValueError(f"Pull request #{pr_number} not found")
|
|
1190
|
+
|
|
1191
|
+
pr_response.raise_for_status()
|
|
1192
|
+
pr = pr_response.json()
|
|
1193
|
+
|
|
1194
|
+
# Update PR body to include issue reference if not already present
|
|
1195
|
+
current_body = pr.get("body", "")
|
|
1196
|
+
issue_ref = f"#{issue_number}"
|
|
1197
|
+
|
|
1198
|
+
if issue_ref not in current_body:
|
|
1199
|
+
# Add issue reference to the body
|
|
1200
|
+
updated_body = current_body or ""
|
|
1201
|
+
if updated_body:
|
|
1202
|
+
updated_body += "\n\n"
|
|
1203
|
+
updated_body += f"Related to #{issue_number}"
|
|
1204
|
+
|
|
1205
|
+
# Update the PR
|
|
1206
|
+
update_response = await self.client.patch(
|
|
1207
|
+
f"/repos/{self.owner}/{self.repo}/pulls/{pr_number}",
|
|
1208
|
+
json={"body": updated_body}
|
|
1209
|
+
)
|
|
1210
|
+
update_response.raise_for_status()
|
|
1211
|
+
|
|
1212
|
+
# Add a comment to the issue about the PR
|
|
1213
|
+
await self.add_comment(
|
|
1214
|
+
Comment(
|
|
1215
|
+
ticket_id=ticket_id,
|
|
1216
|
+
content=f"Linked to pull request #{pr_number}: {pr_url}",
|
|
1217
|
+
author="system",
|
|
1218
|
+
)
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
return {
|
|
1222
|
+
"success": True,
|
|
1223
|
+
"pr_number": pr["number"],
|
|
1224
|
+
"pr_url": pr["html_url"],
|
|
1225
|
+
"pr_title": pr["title"],
|
|
1226
|
+
"pr_state": pr["state"],
|
|
1227
|
+
"linked_issue": issue_number,
|
|
1228
|
+
"message": f"Successfully linked PR #{pr_number} to issue #{issue_number}",
|
|
1229
|
+
}
|
|
1230
|
+
|
|
968
1231
|
async def close(self) -> None:
|
|
969
1232
|
"""Close the HTTP client connection."""
|
|
970
1233
|
await self.client.aclose()
|
mcp_ticketer/adapters/linear.py
CHANGED
|
@@ -1343,6 +1343,226 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1343
1343
|
|
|
1344
1344
|
return result.get("reactionCreate", {}).get("success", False)
|
|
1345
1345
|
|
|
1346
|
+
async def link_to_pull_request(
|
|
1347
|
+
self,
|
|
1348
|
+
ticket_id: str,
|
|
1349
|
+
pr_url: str,
|
|
1350
|
+
pr_number: Optional[int] = None,
|
|
1351
|
+
) -> Dict[str, Any]:
|
|
1352
|
+
"""Link a Linear issue to a GitHub pull request.
|
|
1353
|
+
|
|
1354
|
+
Args:
|
|
1355
|
+
ticket_id: Linear issue identifier (e.g., 'BTA-123')
|
|
1356
|
+
pr_url: GitHub PR URL
|
|
1357
|
+
pr_number: Optional PR number (extracted from URL if not provided)
|
|
1358
|
+
|
|
1359
|
+
Returns:
|
|
1360
|
+
Dictionary with link status and details
|
|
1361
|
+
"""
|
|
1362
|
+
# Parse PR URL to extract details
|
|
1363
|
+
import re
|
|
1364
|
+
pr_pattern = r"github\.com/([^/]+)/([^/]+)/pull/(\d+)"
|
|
1365
|
+
match = re.search(pr_pattern, pr_url)
|
|
1366
|
+
|
|
1367
|
+
if not match:
|
|
1368
|
+
raise ValueError(f"Invalid GitHub PR URL format: {pr_url}")
|
|
1369
|
+
|
|
1370
|
+
owner, repo, extracted_pr_number = match.groups()
|
|
1371
|
+
if not pr_number:
|
|
1372
|
+
pr_number = int(extracted_pr_number)
|
|
1373
|
+
|
|
1374
|
+
# Create an attachment to link the PR
|
|
1375
|
+
create_query = gql("""
|
|
1376
|
+
mutation CreateAttachment($input: AttachmentCreateInput!) {
|
|
1377
|
+
attachmentCreate(input: $input) {
|
|
1378
|
+
attachment {
|
|
1379
|
+
id
|
|
1380
|
+
url
|
|
1381
|
+
title
|
|
1382
|
+
subtitle
|
|
1383
|
+
source
|
|
1384
|
+
}
|
|
1385
|
+
success
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
""")
|
|
1389
|
+
|
|
1390
|
+
# Get the issue ID from the identifier
|
|
1391
|
+
issue = await self.read(ticket_id)
|
|
1392
|
+
if not issue:
|
|
1393
|
+
raise ValueError(f"Issue {ticket_id} not found")
|
|
1394
|
+
|
|
1395
|
+
# Create attachment input
|
|
1396
|
+
attachment_input = {
|
|
1397
|
+
"issueId": issue.metadata.get("linear", {}).get("id"),
|
|
1398
|
+
"url": pr_url,
|
|
1399
|
+
"title": f"Pull Request #{pr_number}",
|
|
1400
|
+
"subtitle": f"{owner}/{repo}",
|
|
1401
|
+
"source": {
|
|
1402
|
+
"type": "githubPr",
|
|
1403
|
+
"data": {
|
|
1404
|
+
"number": pr_number,
|
|
1405
|
+
"owner": owner,
|
|
1406
|
+
"repo": repo,
|
|
1407
|
+
}
|
|
1408
|
+
},
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
async with self.client as session:
|
|
1412
|
+
result = await session.execute(
|
|
1413
|
+
create_query,
|
|
1414
|
+
variable_values={"input": attachment_input}
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
if result.get("attachmentCreate", {}).get("success"):
|
|
1418
|
+
attachment = result["attachmentCreate"]["attachment"]
|
|
1419
|
+
|
|
1420
|
+
# Also add a comment about the PR link
|
|
1421
|
+
comment_text = f"Linked to GitHub PR: {pr_url}"
|
|
1422
|
+
await self.add_comment(
|
|
1423
|
+
Comment(
|
|
1424
|
+
ticket_id=ticket_id,
|
|
1425
|
+
content=comment_text,
|
|
1426
|
+
author="system",
|
|
1427
|
+
)
|
|
1428
|
+
)
|
|
1429
|
+
|
|
1430
|
+
return {
|
|
1431
|
+
"success": True,
|
|
1432
|
+
"attachment_id": attachment["id"],
|
|
1433
|
+
"pr_url": pr_url,
|
|
1434
|
+
"pr_number": pr_number,
|
|
1435
|
+
"linked_issue": ticket_id,
|
|
1436
|
+
"message": f"Successfully linked PR #{pr_number} to issue {ticket_id}",
|
|
1437
|
+
}
|
|
1438
|
+
else:
|
|
1439
|
+
return {
|
|
1440
|
+
"success": False,
|
|
1441
|
+
"pr_url": pr_url,
|
|
1442
|
+
"pr_number": pr_number,
|
|
1443
|
+
"linked_issue": ticket_id,
|
|
1444
|
+
"message": "Failed to create attachment link",
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
async def create_pull_request_for_issue(
|
|
1448
|
+
self,
|
|
1449
|
+
ticket_id: str,
|
|
1450
|
+
github_config: Dict[str, Any],
|
|
1451
|
+
) -> Dict[str, Any]:
|
|
1452
|
+
"""Create a GitHub PR for a Linear issue using GitHub integration.
|
|
1453
|
+
|
|
1454
|
+
This requires GitHub integration to be configured in Linear.
|
|
1455
|
+
|
|
1456
|
+
Args:
|
|
1457
|
+
ticket_id: Linear issue identifier
|
|
1458
|
+
github_config: GitHub configuration including:
|
|
1459
|
+
- owner: GitHub repository owner
|
|
1460
|
+
- repo: GitHub repository name
|
|
1461
|
+
- base_branch: Target branch (default: main)
|
|
1462
|
+
- head_branch: Source branch (auto-generated if not provided)
|
|
1463
|
+
|
|
1464
|
+
Returns:
|
|
1465
|
+
Dictionary with PR creation status
|
|
1466
|
+
"""
|
|
1467
|
+
# Get the issue details
|
|
1468
|
+
issue = await self.read(ticket_id)
|
|
1469
|
+
if not issue:
|
|
1470
|
+
raise ValueError(f"Issue {ticket_id} not found")
|
|
1471
|
+
|
|
1472
|
+
# Generate branch name if not provided
|
|
1473
|
+
head_branch = github_config.get("head_branch")
|
|
1474
|
+
if not head_branch:
|
|
1475
|
+
# Use Linear's branch naming convention
|
|
1476
|
+
# e.g., "bta-123-fix-authentication-bug"
|
|
1477
|
+
safe_title = "-".join(
|
|
1478
|
+
issue.title.lower()
|
|
1479
|
+
.replace("[", "")
|
|
1480
|
+
.replace("]", "")
|
|
1481
|
+
.replace("#", "")
|
|
1482
|
+
.replace("/", "-")
|
|
1483
|
+
.replace("\\", "-")
|
|
1484
|
+
.split()[:5] # Limit to 5 words
|
|
1485
|
+
)
|
|
1486
|
+
head_branch = f"{ticket_id.lower()}-{safe_title}"
|
|
1487
|
+
|
|
1488
|
+
# Update the issue with the branch name
|
|
1489
|
+
update_query = gql("""
|
|
1490
|
+
mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
|
|
1491
|
+
issueUpdate(id: $id, input: $input) {
|
|
1492
|
+
issue {
|
|
1493
|
+
id
|
|
1494
|
+
identifier
|
|
1495
|
+
branchName
|
|
1496
|
+
}
|
|
1497
|
+
success
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
""")
|
|
1501
|
+
|
|
1502
|
+
linear_id = issue.metadata.get("linear", {}).get("id")
|
|
1503
|
+
if not linear_id:
|
|
1504
|
+
# Need to get the full issue ID
|
|
1505
|
+
search_result = await self._search_by_identifier(ticket_id)
|
|
1506
|
+
if not search_result:
|
|
1507
|
+
raise ValueError(f"Could not find Linear ID for issue {ticket_id}")
|
|
1508
|
+
linear_id = search_result["id"]
|
|
1509
|
+
|
|
1510
|
+
async with self.client as session:
|
|
1511
|
+
result = await session.execute(
|
|
1512
|
+
update_query,
|
|
1513
|
+
variable_values={
|
|
1514
|
+
"id": linear_id,
|
|
1515
|
+
"input": {"branchName": head_branch}
|
|
1516
|
+
}
|
|
1517
|
+
)
|
|
1518
|
+
|
|
1519
|
+
if result.get("issueUpdate", {}).get("success"):
|
|
1520
|
+
# Prepare PR metadata to return
|
|
1521
|
+
pr_metadata = {
|
|
1522
|
+
"branch_name": head_branch,
|
|
1523
|
+
"issue_id": ticket_id,
|
|
1524
|
+
"issue_title": issue.title,
|
|
1525
|
+
"issue_description": issue.description,
|
|
1526
|
+
"github_owner": github_config.get("owner"),
|
|
1527
|
+
"github_repo": github_config.get("repo"),
|
|
1528
|
+
"base_branch": github_config.get("base_branch", "main"),
|
|
1529
|
+
"message": f"Branch name '{head_branch}' set for issue {ticket_id}. Use GitHub integration or API to create the actual PR.",
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
# Add a comment about the branch
|
|
1533
|
+
await self.add_comment(
|
|
1534
|
+
Comment(
|
|
1535
|
+
ticket_id=ticket_id,
|
|
1536
|
+
content=f"Branch created: `{head_branch}`\nReady for pull request to `{pr_metadata['base_branch']}`",
|
|
1537
|
+
author="system",
|
|
1538
|
+
)
|
|
1539
|
+
)
|
|
1540
|
+
|
|
1541
|
+
return pr_metadata
|
|
1542
|
+
else:
|
|
1543
|
+
raise ValueError(f"Failed to update issue {ticket_id} with branch name")
|
|
1544
|
+
|
|
1545
|
+
async def _search_by_identifier(self, identifier: str) -> Optional[Dict[str, Any]]:
|
|
1546
|
+
"""Search for an issue by its identifier."""
|
|
1547
|
+
search_query = gql("""
|
|
1548
|
+
query SearchIssue($identifier: String!) {
|
|
1549
|
+
issue(id: $identifier) {
|
|
1550
|
+
id
|
|
1551
|
+
identifier
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
""")
|
|
1555
|
+
|
|
1556
|
+
try:
|
|
1557
|
+
async with self.client as session:
|
|
1558
|
+
result = await session.execute(
|
|
1559
|
+
search_query,
|
|
1560
|
+
variable_values={"identifier": identifier}
|
|
1561
|
+
)
|
|
1562
|
+
return result.get("issue")
|
|
1563
|
+
except:
|
|
1564
|
+
return None
|
|
1565
|
+
|
|
1346
1566
|
async def close(self) -> None:
|
|
1347
1567
|
"""Close the GraphQL client connection."""
|
|
1348
1568
|
if hasattr(self.client, 'close_async'):
|
mcp_ticketer/cli/main.py
CHANGED
|
@@ -18,6 +18,7 @@ from ..core.models import SearchQuery
|
|
|
18
18
|
from ..adapters import AITrackdownAdapter
|
|
19
19
|
from ..queue import Queue, QueueStatus, WorkerManager
|
|
20
20
|
from .queue_commands import app as queue_app
|
|
21
|
+
from ..__version__ import __version__
|
|
21
22
|
|
|
22
23
|
# Load environment variables
|
|
23
24
|
load_dotenv()
|
|
@@ -29,6 +30,31 @@ app = typer.Typer(
|
|
|
29
30
|
)
|
|
30
31
|
console = Console()
|
|
31
32
|
|
|
33
|
+
|
|
34
|
+
def version_callback(value: bool):
|
|
35
|
+
"""Print version and exit."""
|
|
36
|
+
if value:
|
|
37
|
+
console.print(f"mcp-ticketer version {__version__}")
|
|
38
|
+
raise typer.Exit()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.callback()
|
|
42
|
+
def main_callback(
|
|
43
|
+
version: bool = typer.Option(
|
|
44
|
+
None,
|
|
45
|
+
"--version",
|
|
46
|
+
"-v",
|
|
47
|
+
callback=version_callback,
|
|
48
|
+
is_eager=True,
|
|
49
|
+
help="Show version and exit"
|
|
50
|
+
),
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
MCP Ticketer - Universal ticket management interface.
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
32
58
|
# Configuration file management
|
|
33
59
|
CONFIG_FILE = Path.home() / ".mcp-ticketer" / "config.json"
|
|
34
60
|
|
mcp_ticketer/mcp/server.py
CHANGED
|
@@ -63,6 +63,10 @@ class MCPTicketServer:
|
|
|
63
63
|
result = await self._handle_comment(params)
|
|
64
64
|
elif method == "ticket/status":
|
|
65
65
|
result = await self._handle_queue_status(params)
|
|
66
|
+
elif method == "ticket/create_pr":
|
|
67
|
+
result = await self._handle_create_pr(params)
|
|
68
|
+
elif method == "ticket/link_pr":
|
|
69
|
+
result = await self._handle_link_pr(params)
|
|
66
70
|
elif method == "tools/list":
|
|
67
71
|
result = await self._handle_tools_list()
|
|
68
72
|
elif method == "tools/call":
|
|
@@ -134,11 +138,72 @@ class MCPTicketServer:
|
|
|
134
138
|
manager = WorkerManager()
|
|
135
139
|
manager.start_if_needed()
|
|
136
140
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
# Check if async mode is requested (for backward compatibility)
|
|
142
|
+
if params.get("async_mode", False):
|
|
143
|
+
return {
|
|
144
|
+
"queue_id": queue_id,
|
|
145
|
+
"status": "queued",
|
|
146
|
+
"message": f"Ticket creation queued with ID: {queue_id}"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# Poll for completion with timeout (default synchronous behavior)
|
|
150
|
+
max_wait_time = params.get("timeout", 30) # seconds, allow override
|
|
151
|
+
poll_interval = 0.5 # seconds
|
|
152
|
+
start_time = asyncio.get_event_loop().time()
|
|
153
|
+
|
|
154
|
+
while True:
|
|
155
|
+
# Check queue status
|
|
156
|
+
item = queue.get_item(queue_id)
|
|
157
|
+
|
|
158
|
+
if not item:
|
|
159
|
+
return {
|
|
160
|
+
"queue_id": queue_id,
|
|
161
|
+
"status": "error",
|
|
162
|
+
"error": f"Queue item {queue_id} not found"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# If completed, return with ticket ID
|
|
166
|
+
if item.status == QueueStatus.COMPLETED:
|
|
167
|
+
response = {
|
|
168
|
+
"queue_id": queue_id,
|
|
169
|
+
"status": "completed",
|
|
170
|
+
"title": params["title"]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Add ticket ID and other result data if available
|
|
174
|
+
if item.result:
|
|
175
|
+
response["ticket_id"] = item.result.get("id")
|
|
176
|
+
if "state" in item.result:
|
|
177
|
+
response["state"] = item.result["state"]
|
|
178
|
+
# Try to construct URL if we have enough information
|
|
179
|
+
if response.get("ticket_id"):
|
|
180
|
+
# This is adapter-specific, but we can add URL generation later
|
|
181
|
+
response["id"] = response["ticket_id"] # Also include as "id" for compatibility
|
|
182
|
+
|
|
183
|
+
response["message"] = f"Ticket created successfully: {response.get('ticket_id', queue_id)}"
|
|
184
|
+
return response
|
|
185
|
+
|
|
186
|
+
# If failed, return error
|
|
187
|
+
if item.status == QueueStatus.FAILED:
|
|
188
|
+
return {
|
|
189
|
+
"queue_id": queue_id,
|
|
190
|
+
"status": "failed",
|
|
191
|
+
"error": item.error_message or "Ticket creation failed",
|
|
192
|
+
"title": params["title"]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Check timeout
|
|
196
|
+
elapsed = asyncio.get_event_loop().time() - start_time
|
|
197
|
+
if elapsed > max_wait_time:
|
|
198
|
+
return {
|
|
199
|
+
"queue_id": queue_id,
|
|
200
|
+
"status": "timeout",
|
|
201
|
+
"message": f"Ticket creation timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
|
|
202
|
+
"title": params["title"]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Wait before next poll
|
|
206
|
+
await asyncio.sleep(poll_interval)
|
|
142
207
|
|
|
143
208
|
async def _handle_read(self, params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
144
209
|
"""Handle ticket read."""
|
|
@@ -162,11 +227,60 @@ class MCPTicketServer:
|
|
|
162
227
|
manager = WorkerManager()
|
|
163
228
|
manager.start_if_needed()
|
|
164
229
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
230
|
+
# Poll for completion with timeout
|
|
231
|
+
max_wait_time = 30 # seconds
|
|
232
|
+
poll_interval = 0.5 # seconds
|
|
233
|
+
start_time = asyncio.get_event_loop().time()
|
|
234
|
+
|
|
235
|
+
while True:
|
|
236
|
+
# Check queue status
|
|
237
|
+
item = queue.get_item(queue_id)
|
|
238
|
+
|
|
239
|
+
if not item:
|
|
240
|
+
return {
|
|
241
|
+
"queue_id": queue_id,
|
|
242
|
+
"status": "error",
|
|
243
|
+
"error": f"Queue item {queue_id} not found"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# If completed, return with ticket ID
|
|
247
|
+
if item.status == QueueStatus.COMPLETED:
|
|
248
|
+
response = {
|
|
249
|
+
"queue_id": queue_id,
|
|
250
|
+
"status": "completed",
|
|
251
|
+
"ticket_id": params["ticket_id"]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# Add result data if available
|
|
255
|
+
if item.result:
|
|
256
|
+
if item.result.get("id"):
|
|
257
|
+
response["ticket_id"] = item.result["id"]
|
|
258
|
+
response["success"] = item.result.get("success", True)
|
|
259
|
+
|
|
260
|
+
response["message"] = f"Ticket updated successfully: {response['ticket_id']}"
|
|
261
|
+
return response
|
|
262
|
+
|
|
263
|
+
# If failed, return error
|
|
264
|
+
if item.status == QueueStatus.FAILED:
|
|
265
|
+
return {
|
|
266
|
+
"queue_id": queue_id,
|
|
267
|
+
"status": "failed",
|
|
268
|
+
"error": item.error_message or "Ticket update failed",
|
|
269
|
+
"ticket_id": params["ticket_id"]
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# Check timeout
|
|
273
|
+
elapsed = asyncio.get_event_loop().time() - start_time
|
|
274
|
+
if elapsed > max_wait_time:
|
|
275
|
+
return {
|
|
276
|
+
"queue_id": queue_id,
|
|
277
|
+
"status": "timeout",
|
|
278
|
+
"message": f"Ticket update timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
|
|
279
|
+
"ticket_id": params["ticket_id"]
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# Wait before next poll
|
|
283
|
+
await asyncio.sleep(poll_interval)
|
|
170
284
|
|
|
171
285
|
async def _handle_delete(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
172
286
|
"""Handle ticket deletion."""
|
|
@@ -220,11 +334,61 @@ class MCPTicketServer:
|
|
|
220
334
|
manager = WorkerManager()
|
|
221
335
|
manager.start_if_needed()
|
|
222
336
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
337
|
+
# Poll for completion with timeout
|
|
338
|
+
max_wait_time = 30 # seconds
|
|
339
|
+
poll_interval = 0.5 # seconds
|
|
340
|
+
start_time = asyncio.get_event_loop().time()
|
|
341
|
+
|
|
342
|
+
while True:
|
|
343
|
+
# Check queue status
|
|
344
|
+
item = queue.get_item(queue_id)
|
|
345
|
+
|
|
346
|
+
if not item:
|
|
347
|
+
return {
|
|
348
|
+
"queue_id": queue_id,
|
|
349
|
+
"status": "error",
|
|
350
|
+
"error": f"Queue item {queue_id} not found"
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# If completed, return with ticket ID
|
|
354
|
+
if item.status == QueueStatus.COMPLETED:
|
|
355
|
+
response = {
|
|
356
|
+
"queue_id": queue_id,
|
|
357
|
+
"status": "completed",
|
|
358
|
+
"ticket_id": params["ticket_id"],
|
|
359
|
+
"state": params["target_state"]
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
# Add result data if available
|
|
363
|
+
if item.result:
|
|
364
|
+
if item.result.get("id"):
|
|
365
|
+
response["ticket_id"] = item.result["id"]
|
|
366
|
+
response["success"] = item.result.get("success", True)
|
|
367
|
+
|
|
368
|
+
response["message"] = f"State transition completed successfully: {response['ticket_id']} → {params['target_state']}"
|
|
369
|
+
return response
|
|
370
|
+
|
|
371
|
+
# If failed, return error
|
|
372
|
+
if item.status == QueueStatus.FAILED:
|
|
373
|
+
return {
|
|
374
|
+
"queue_id": queue_id,
|
|
375
|
+
"status": "failed",
|
|
376
|
+
"error": item.error_message or "State transition failed",
|
|
377
|
+
"ticket_id": params["ticket_id"]
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
# Check timeout
|
|
381
|
+
elapsed = asyncio.get_event_loop().time() - start_time
|
|
382
|
+
if elapsed > max_wait_time:
|
|
383
|
+
return {
|
|
384
|
+
"queue_id": queue_id,
|
|
385
|
+
"status": "timeout",
|
|
386
|
+
"message": f"State transition timed out after {max_wait_time} seconds. Use ticket_status with queue_id to check status.",
|
|
387
|
+
"ticket_id": params["ticket_id"]
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# Wait before next poll
|
|
391
|
+
await asyncio.sleep(poll_interval)
|
|
228
392
|
|
|
229
393
|
async def _handle_comment(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
230
394
|
"""Handle comment operations."""
|
|
@@ -298,6 +462,144 @@ class MCPTicketServer:
|
|
|
298
462
|
|
|
299
463
|
return response
|
|
300
464
|
|
|
465
|
+
async def _handle_create_pr(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
466
|
+
"""Handle PR creation for a ticket."""
|
|
467
|
+
ticket_id = params.get("ticket_id")
|
|
468
|
+
if not ticket_id:
|
|
469
|
+
raise ValueError("ticket_id is required")
|
|
470
|
+
|
|
471
|
+
# Check if adapter supports PR creation
|
|
472
|
+
adapter_name = self.adapter.__class__.__name__.lower()
|
|
473
|
+
|
|
474
|
+
if "github" in adapter_name:
|
|
475
|
+
# GitHub adapter supports direct PR creation
|
|
476
|
+
from ..adapters.github import GitHubAdapter
|
|
477
|
+
if isinstance(self.adapter, GitHubAdapter):
|
|
478
|
+
try:
|
|
479
|
+
result = await self.adapter.create_pull_request(
|
|
480
|
+
ticket_id=ticket_id,
|
|
481
|
+
base_branch=params.get("base_branch", "main"),
|
|
482
|
+
head_branch=params.get("head_branch"),
|
|
483
|
+
title=params.get("title"),
|
|
484
|
+
body=params.get("body"),
|
|
485
|
+
draft=params.get("draft", False),
|
|
486
|
+
)
|
|
487
|
+
return {
|
|
488
|
+
"success": True,
|
|
489
|
+
"pr_number": result.get("number"),
|
|
490
|
+
"pr_url": result.get("url"),
|
|
491
|
+
"branch": result.get("branch"),
|
|
492
|
+
"linked_issue": result.get("linked_issue"),
|
|
493
|
+
"message": f"Pull request created successfully: {result.get('url')}",
|
|
494
|
+
}
|
|
495
|
+
except Exception as e:
|
|
496
|
+
return {
|
|
497
|
+
"success": False,
|
|
498
|
+
"error": str(e),
|
|
499
|
+
"ticket_id": ticket_id,
|
|
500
|
+
}
|
|
501
|
+
elif "linear" in adapter_name:
|
|
502
|
+
# Linear adapter needs GitHub config for PR creation
|
|
503
|
+
from ..adapters.linear import LinearAdapter
|
|
504
|
+
if isinstance(self.adapter, LinearAdapter):
|
|
505
|
+
# For Linear, we prepare the branch and metadata but can't create the actual PR
|
|
506
|
+
# without GitHub integration configured
|
|
507
|
+
try:
|
|
508
|
+
github_config = {
|
|
509
|
+
"owner": params.get("github_owner"),
|
|
510
|
+
"repo": params.get("github_repo"),
|
|
511
|
+
"base_branch": params.get("base_branch", "main"),
|
|
512
|
+
"head_branch": params.get("head_branch"),
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
# Validate GitHub config for Linear
|
|
516
|
+
if not github_config.get("owner") or not github_config.get("repo"):
|
|
517
|
+
return {
|
|
518
|
+
"success": False,
|
|
519
|
+
"error": "GitHub owner and repo are required for Linear PR creation",
|
|
520
|
+
"ticket_id": ticket_id,
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
result = await self.adapter.create_pull_request_for_issue(
|
|
524
|
+
ticket_id=ticket_id,
|
|
525
|
+
github_config=github_config,
|
|
526
|
+
)
|
|
527
|
+
return {
|
|
528
|
+
"success": True,
|
|
529
|
+
"branch_name": result.get("branch_name"),
|
|
530
|
+
"ticket_id": ticket_id,
|
|
531
|
+
"message": result.get("message"),
|
|
532
|
+
"github_config": {
|
|
533
|
+
"owner": result.get("github_owner"),
|
|
534
|
+
"repo": result.get("github_repo"),
|
|
535
|
+
"base_branch": result.get("base_branch"),
|
|
536
|
+
},
|
|
537
|
+
}
|
|
538
|
+
except Exception as e:
|
|
539
|
+
return {
|
|
540
|
+
"success": False,
|
|
541
|
+
"error": str(e),
|
|
542
|
+
"ticket_id": ticket_id,
|
|
543
|
+
}
|
|
544
|
+
else:
|
|
545
|
+
return {
|
|
546
|
+
"success": False,
|
|
547
|
+
"error": f"PR creation not supported for adapter: {adapter_name}",
|
|
548
|
+
"ticket_id": ticket_id,
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async def _handle_link_pr(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
552
|
+
"""Handle linking an existing PR to a ticket."""
|
|
553
|
+
ticket_id = params.get("ticket_id")
|
|
554
|
+
pr_url = params.get("pr_url")
|
|
555
|
+
|
|
556
|
+
if not ticket_id:
|
|
557
|
+
raise ValueError("ticket_id is required")
|
|
558
|
+
if not pr_url:
|
|
559
|
+
raise ValueError("pr_url is required")
|
|
560
|
+
|
|
561
|
+
adapter_name = self.adapter.__class__.__name__.lower()
|
|
562
|
+
|
|
563
|
+
if "github" in adapter_name:
|
|
564
|
+
from ..adapters.github import GitHubAdapter
|
|
565
|
+
if isinstance(self.adapter, GitHubAdapter):
|
|
566
|
+
try:
|
|
567
|
+
result = await self.adapter.link_existing_pull_request(
|
|
568
|
+
ticket_id=ticket_id,
|
|
569
|
+
pr_url=pr_url,
|
|
570
|
+
)
|
|
571
|
+
return result
|
|
572
|
+
except Exception as e:
|
|
573
|
+
return {
|
|
574
|
+
"success": False,
|
|
575
|
+
"error": str(e),
|
|
576
|
+
"ticket_id": ticket_id,
|
|
577
|
+
"pr_url": pr_url,
|
|
578
|
+
}
|
|
579
|
+
elif "linear" in adapter_name:
|
|
580
|
+
from ..adapters.linear import LinearAdapter
|
|
581
|
+
if isinstance(self.adapter, LinearAdapter):
|
|
582
|
+
try:
|
|
583
|
+
result = await self.adapter.link_to_pull_request(
|
|
584
|
+
ticket_id=ticket_id,
|
|
585
|
+
pr_url=pr_url,
|
|
586
|
+
)
|
|
587
|
+
return result
|
|
588
|
+
except Exception as e:
|
|
589
|
+
return {
|
|
590
|
+
"success": False,
|
|
591
|
+
"error": str(e),
|
|
592
|
+
"ticket_id": ticket_id,
|
|
593
|
+
"pr_url": pr_url,
|
|
594
|
+
}
|
|
595
|
+
else:
|
|
596
|
+
return {
|
|
597
|
+
"success": False,
|
|
598
|
+
"error": f"PR linking not supported for adapter: {adapter_name}",
|
|
599
|
+
"ticket_id": ticket_id,
|
|
600
|
+
"pr_url": pr_url,
|
|
601
|
+
}
|
|
602
|
+
|
|
301
603
|
async def _handle_initialize(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
302
604
|
"""Handle initialize request from MCP client.
|
|
303
605
|
|
|
@@ -324,6 +626,34 @@ class MCPTicketServer:
|
|
|
324
626
|
"""List available MCP tools."""
|
|
325
627
|
return {
|
|
326
628
|
"tools": [
|
|
629
|
+
{
|
|
630
|
+
"name": "ticket_create_pr",
|
|
631
|
+
"description": "Create a GitHub PR linked to a ticket",
|
|
632
|
+
"inputSchema": {
|
|
633
|
+
"type": "object",
|
|
634
|
+
"properties": {
|
|
635
|
+
"ticket_id": {"type": "string", "description": "Ticket ID to link the PR to"},
|
|
636
|
+
"base_branch": {"type": "string", "description": "Target branch for the PR", "default": "main"},
|
|
637
|
+
"head_branch": {"type": "string", "description": "Source branch name (auto-generated if not provided)"},
|
|
638
|
+
"title": {"type": "string", "description": "PR title (uses ticket title if not provided)"},
|
|
639
|
+
"body": {"type": "string", "description": "PR description (auto-generated with issue link if not provided)"},
|
|
640
|
+
"draft": {"type": "boolean", "description": "Create as draft PR", "default": False},
|
|
641
|
+
},
|
|
642
|
+
"required": ["ticket_id"]
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
"name": "ticket_link_pr",
|
|
647
|
+
"description": "Link an existing PR to a ticket",
|
|
648
|
+
"inputSchema": {
|
|
649
|
+
"type": "object",
|
|
650
|
+
"properties": {
|
|
651
|
+
"ticket_id": {"type": "string", "description": "Ticket ID to link the PR to"},
|
|
652
|
+
"pr_url": {"type": "string", "description": "GitHub PR URL to link"},
|
|
653
|
+
},
|
|
654
|
+
"required": ["ticket_id", "pr_url"]
|
|
655
|
+
}
|
|
656
|
+
},
|
|
327
657
|
{
|
|
328
658
|
"name": "ticket_create",
|
|
329
659
|
"description": "Create a new ticket",
|
|
@@ -428,6 +758,10 @@ class MCPTicketServer:
|
|
|
428
758
|
result = await self._handle_search(arguments)
|
|
429
759
|
elif tool_name == "ticket_status":
|
|
430
760
|
result = await self._handle_queue_status(arguments)
|
|
761
|
+
elif tool_name == "ticket_create_pr":
|
|
762
|
+
result = await self._handle_create_pr(arguments)
|
|
763
|
+
elif tool_name == "ticket_link_pr":
|
|
764
|
+
result = await self._handle_link_pr(arguments)
|
|
431
765
|
else:
|
|
432
766
|
return {
|
|
433
767
|
"content": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-ticketer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.11
|
|
4
4
|
Summary: Universal ticket management interface for AI agents with MCP support
|
|
5
5
|
Author-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
6
6
|
Maintainer-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
@@ -33,13 +33,14 @@ Classifier: Typing :: Typed
|
|
|
33
33
|
Requires-Python: >=3.9
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
License-File: LICENSE
|
|
36
|
+
Requires-Dist: gql[httpx]>=3.0.0
|
|
37
|
+
Requires-Dist: httpx>=0.25.0
|
|
38
|
+
Requires-Dist: psutil>=5.9.0
|
|
36
39
|
Requires-Dist: pydantic>=2.0
|
|
37
|
-
Requires-Dist:
|
|
40
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
38
41
|
Requires-Dist: rich>=13.0.0
|
|
39
|
-
Requires-Dist:
|
|
42
|
+
Requires-Dist: typer>=0.9.0
|
|
40
43
|
Requires-Dist: typing-extensions>=4.8.0
|
|
41
|
-
Requires-Dist: python-dotenv>=1.0.0
|
|
42
|
-
Requires-Dist: psutil>=5.9.0
|
|
43
44
|
Provides-Extra: all
|
|
44
45
|
Requires-Dist: mcp-ticketer[github,jira,linear,mcp]; extra == "all"
|
|
45
46
|
Provides-Extra: dev
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
mcp_ticketer/__init__.py,sha256=ayPQdFr6msypD06_G96a1H0bdFCT1m1wDtv8MZBpY4I,496
|
|
2
|
-
mcp_ticketer/__version__.py,sha256=
|
|
2
|
+
mcp_ticketer/__version__.py,sha256=Ym9ng_TEiLSfqVAhPCtwjrIqiM8S9cAUurRI6mpYEyg,1115
|
|
3
3
|
mcp_ticketer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
mcp_ticketer/adapters/__init__.py,sha256=_QRLaX38EUsL-kMvJITY0lYHvrq_ip9Qw4Q1YLavJSo,283
|
|
5
5
|
mcp_ticketer/adapters/aitrackdown.py,sha256=ICNimTtF6rPajuVoVEpmdw2TfjYjnWvao8prUwukNn0,15210
|
|
6
|
-
mcp_ticketer/adapters/github.py,sha256=
|
|
6
|
+
mcp_ticketer/adapters/github.py,sha256=onT8NhYaf9fIw2eCOTbZSkk7q4IoM7ZADRvRl9qrUz8,43850
|
|
7
7
|
mcp_ticketer/adapters/jira.py,sha256=rd-8PseTsRyQNPjsrUJ8vJ8vfBpa6HWFBieOUyvw0Tg,28954
|
|
8
|
-
mcp_ticketer/adapters/linear.py,sha256=
|
|
8
|
+
mcp_ticketer/adapters/linear.py,sha256=neTxVy-QD23tTI7XKtnc5CBCpm3yVCULlgxG5oFSQI4,51752
|
|
9
9
|
mcp_ticketer/cache/__init__.py,sha256=MSi3GLXancfP2-edPC9TFAJk7r0j6H5-XmpMHnkGPbI,137
|
|
10
10
|
mcp_ticketer/cache/memory.py,sha256=gTzv-xF7qGfiYVUjG7lnzo0ZcqgXQajMl4NAYUcaytg,5133
|
|
11
11
|
mcp_ticketer/cli/__init__.py,sha256=YeljyLtv906TqkvRuEPhmKO-Uk0CberQ9I6kx1tx2UA,88
|
|
12
|
-
mcp_ticketer/cli/main.py,sha256=
|
|
12
|
+
mcp_ticketer/cli/main.py,sha256=adz3YXGnw7GY1h9jYgmIfYizf-z4ddhx2adFjncpZ3A,28294
|
|
13
13
|
mcp_ticketer/cli/queue_commands.py,sha256=f3pEHKZ43dBHEIoCBvdfvjfMB9_WJltps9ATwTzorY0,8160
|
|
14
14
|
mcp_ticketer/cli/utils.py,sha256=NxsS91vKA8xZnDXKU2y0Gcyc4I_ctRyJj-wT7Xd1Q_Q,18589
|
|
15
15
|
mcp_ticketer/core/__init__.py,sha256=NA-rDvwuAOZ9sUZVYJOWp8bR6mOBG8w_5lpWTT75JNI,318
|
|
@@ -20,16 +20,16 @@ mcp_ticketer/core/mappers.py,sha256=8I4jcqDqoQEdWlteDMpVeVF3Wo0fDCkmFPRr8oNv8gA,
|
|
|
20
20
|
mcp_ticketer/core/models.py,sha256=K-bLuU_DNNVgjHnVFzAIbSa0kJwT2I3Hj24sCklwIYo,4374
|
|
21
21
|
mcp_ticketer/core/registry.py,sha256=fwje0fnjp0YKPZ0SrVWk82SMNLs7CD0JlHQmx7SigNo,3537
|
|
22
22
|
mcp_ticketer/mcp/__init__.py,sha256=Bvzof9vBu6VwcXcIZK8RgKv6ycRV9tDlO-9TUmd8zqQ,122
|
|
23
|
-
mcp_ticketer/mcp/server.py,sha256=
|
|
23
|
+
mcp_ticketer/mcp/server.py,sha256=TDuU8ChZC2QYSRo0uGHkVReblTf--hriOjxo-pSAF_Y,34068
|
|
24
24
|
mcp_ticketer/queue/__init__.py,sha256=xHBoUwor8ZdO8bIHc7nP25EsAp5Si5Co4g_8ybb7fes,230
|
|
25
25
|
mcp_ticketer/queue/__main__.py,sha256=kQd6iOCKbbFqpRdbIRavuI4_G7-oE898JE4a0yLEYPE,108
|
|
26
26
|
mcp_ticketer/queue/manager.py,sha256=79AH9oUxdBXH3lmJ3kIlFf2GQkWHL6XB6u5JqVWPq60,7571
|
|
27
27
|
mcp_ticketer/queue/queue.py,sha256=UgbIChWPiyI7BJNQ9DYA92D2jVMMtmVWBzotI5ML51A,11394
|
|
28
28
|
mcp_ticketer/queue/run_worker.py,sha256=HFoykfDpOoz8OUxWbQ2Fka_UlGrYwjPVZ-DEimGFH9o,802
|
|
29
29
|
mcp_ticketer/queue/worker.py,sha256=cVjHR_kfnGKAkiUg0HuXCnbKeKNBBEuj0XZHgIuIn4k,14017
|
|
30
|
-
mcp_ticketer-0.1.
|
|
31
|
-
mcp_ticketer-0.1.
|
|
32
|
-
mcp_ticketer-0.1.
|
|
33
|
-
mcp_ticketer-0.1.
|
|
34
|
-
mcp_ticketer-0.1.
|
|
35
|
-
mcp_ticketer-0.1.
|
|
30
|
+
mcp_ticketer-0.1.11.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
|
|
31
|
+
mcp_ticketer-0.1.11.dist-info/METADATA,sha256=GjLKPEBTziutLdEpztLyQkGvxMCpiHArYsdSKltJwwA,11211
|
|
32
|
+
mcp_ticketer-0.1.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
33
|
+
mcp_ticketer-0.1.11.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
|
|
34
|
+
mcp_ticketer-0.1.11.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
|
|
35
|
+
mcp_ticketer-0.1.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|