mcp-ticketer 0.1.8__py3-none-any.whl → 0.1.12__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/__init__.py +8 -1
- mcp_ticketer/adapters/aitrackdown.py +9 -5
- mcp_ticketer/adapters/github.py +263 -0
- mcp_ticketer/adapters/hybrid.py +505 -0
- mcp_ticketer/adapters/linear.py +220 -0
- mcp_ticketer/cli/configure.py +532 -0
- mcp_ticketer/cli/main.py +107 -0
- mcp_ticketer/cli/migrate_config.py +204 -0
- mcp_ticketer/core/project_config.py +553 -0
- mcp_ticketer/mcp/server.py +349 -15
- mcp_ticketer/queue/queue.py +4 -1
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/METADATA +6 -5
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/RECORD +18 -14
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/top_level.txt +0 -0
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'):
|