|
5 | 5 | using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; |
6 | 6 | using Microsoft.VisualStudio.Services.Common; |
7 | 7 | using Microsoft.VisualStudio.Services.WebApi; |
| 8 | +using Microsoft.VisualStudio.Services.WebApi.Patch; |
| 9 | +using Microsoft.VisualStudio.Services.WebApi.Patch.Json; |
8 | 10 | using Viamus.Azure.Devops.Mcp.Server.Configuration; |
9 | 11 | using Viamus.Azure.Devops.Mcp.Server.Models; |
10 | 12 |
|
@@ -491,6 +493,290 @@ public async Task<WorkItemCommentDto> AddWorkItemCommentAsync( |
491 | 493 | } |
492 | 494 | } |
493 | 495 |
|
| 496 | + public async Task<WorkItemDto> CreateWorkItemAsync( |
| 497 | + string project, |
| 498 | + string workItemType, |
| 499 | + string title, |
| 500 | + string? description = null, |
| 501 | + string? assignedTo = null, |
| 502 | + string? areaPath = null, |
| 503 | + string? iterationPath = null, |
| 504 | + string? state = null, |
| 505 | + int? priority = null, |
| 506 | + int? parentId = null, |
| 507 | + string? tags = null, |
| 508 | + Dictionary<string, string>? additionalFields = null, |
| 509 | + CancellationToken cancellationToken = default) |
| 510 | + { |
| 511 | + try |
| 512 | + { |
| 513 | + _logger.LogDebug("Creating work item of type {WorkItemType} in project {Project}", workItemType, project); |
| 514 | + |
| 515 | + var patchDocument = new JsonPatchDocument(); |
| 516 | + |
| 517 | + patchDocument.Add(new JsonPatchOperation |
| 518 | + { |
| 519 | + Operation = Operation.Add, |
| 520 | + Path = "/fields/System.Title", |
| 521 | + Value = title |
| 522 | + }); |
| 523 | + |
| 524 | + if (!string.IsNullOrEmpty(description)) |
| 525 | + { |
| 526 | + patchDocument.Add(new JsonPatchOperation |
| 527 | + { |
| 528 | + Operation = Operation.Add, |
| 529 | + Path = "/fields/System.Description", |
| 530 | + Value = description |
| 531 | + }); |
| 532 | + } |
| 533 | + |
| 534 | + if (!string.IsNullOrEmpty(assignedTo)) |
| 535 | + { |
| 536 | + patchDocument.Add(new JsonPatchOperation |
| 537 | + { |
| 538 | + Operation = Operation.Add, |
| 539 | + Path = "/fields/System.AssignedTo", |
| 540 | + Value = assignedTo |
| 541 | + }); |
| 542 | + } |
| 543 | + |
| 544 | + if (!string.IsNullOrEmpty(areaPath)) |
| 545 | + { |
| 546 | + patchDocument.Add(new JsonPatchOperation |
| 547 | + { |
| 548 | + Operation = Operation.Add, |
| 549 | + Path = "/fields/System.AreaPath", |
| 550 | + Value = areaPath |
| 551 | + }); |
| 552 | + } |
| 553 | + |
| 554 | + if (!string.IsNullOrEmpty(iterationPath)) |
| 555 | + { |
| 556 | + patchDocument.Add(new JsonPatchOperation |
| 557 | + { |
| 558 | + Operation = Operation.Add, |
| 559 | + Path = "/fields/System.IterationPath", |
| 560 | + Value = iterationPath |
| 561 | + }); |
| 562 | + } |
| 563 | + |
| 564 | + if (!string.IsNullOrEmpty(state)) |
| 565 | + { |
| 566 | + patchDocument.Add(new JsonPatchOperation |
| 567 | + { |
| 568 | + Operation = Operation.Add, |
| 569 | + Path = "/fields/System.State", |
| 570 | + Value = state |
| 571 | + }); |
| 572 | + } |
| 573 | + |
| 574 | + if (priority.HasValue) |
| 575 | + { |
| 576 | + patchDocument.Add(new JsonPatchOperation |
| 577 | + { |
| 578 | + Operation = Operation.Add, |
| 579 | + Path = "/fields/Microsoft.VSTS.Common.Priority", |
| 580 | + Value = priority.Value |
| 581 | + }); |
| 582 | + } |
| 583 | + |
| 584 | + if (!string.IsNullOrEmpty(tags)) |
| 585 | + { |
| 586 | + patchDocument.Add(new JsonPatchOperation |
| 587 | + { |
| 588 | + Operation = Operation.Add, |
| 589 | + Path = "/fields/System.Tags", |
| 590 | + Value = tags |
| 591 | + }); |
| 592 | + } |
| 593 | + |
| 594 | + if (parentId.HasValue) |
| 595 | + { |
| 596 | + patchDocument.Add(new JsonPatchOperation |
| 597 | + { |
| 598 | + Operation = Operation.Add, |
| 599 | + Path = "/relations/-", |
| 600 | + Value = new |
| 601 | + { |
| 602 | + rel = "System.LinkTypes.Hierarchy-Reverse", |
| 603 | + url = $"{_options.OrganizationUrl}/_apis/wit/workItems/{parentId.Value}", |
| 604 | + attributes = new { comment = "Parent" } |
| 605 | + } |
| 606 | + }); |
| 607 | + } |
| 608 | + |
| 609 | + if (additionalFields != null) |
| 610 | + { |
| 611 | + foreach (var field in additionalFields) |
| 612 | + { |
| 613 | + var path = field.Key.StartsWith("/fields/") |
| 614 | + ? field.Key |
| 615 | + : $"/fields/{field.Key}"; |
| 616 | + |
| 617 | + patchDocument.Add(new JsonPatchOperation |
| 618 | + { |
| 619 | + Operation = Operation.Add, |
| 620 | + Path = path, |
| 621 | + Value = field.Value |
| 622 | + }); |
| 623 | + } |
| 624 | + } |
| 625 | + |
| 626 | + var result = await _witClient.CreateWorkItemAsync( |
| 627 | + document: patchDocument, |
| 628 | + project: project, |
| 629 | + type: workItemType, |
| 630 | + cancellationToken: cancellationToken); |
| 631 | + |
| 632 | + return MapToDto(result, includeAllFields: true); |
| 633 | + } |
| 634 | + catch (Exception ex) |
| 635 | + { |
| 636 | + _logger.LogError(ex, "Error creating work item of type {WorkItemType} in project {Project}", workItemType, project); |
| 637 | + throw; |
| 638 | + } |
| 639 | + } |
| 640 | + |
| 641 | + public async Task<WorkItemDto> UpdateWorkItemAsync( |
| 642 | + int workItemId, |
| 643 | + string? title = null, |
| 644 | + string? description = null, |
| 645 | + string? assignedTo = null, |
| 646 | + string? state = null, |
| 647 | + string? areaPath = null, |
| 648 | + string? iterationPath = null, |
| 649 | + int? priority = null, |
| 650 | + string? tags = null, |
| 651 | + Dictionary<string, string>? additionalFields = null, |
| 652 | + string? project = null, |
| 653 | + CancellationToken cancellationToken = default) |
| 654 | + { |
| 655 | + try |
| 656 | + { |
| 657 | + _logger.LogDebug("Updating work item {WorkItemId}", workItemId); |
| 658 | + |
| 659 | + var patchDocument = new JsonPatchDocument(); |
| 660 | + |
| 661 | + if (title != null) |
| 662 | + { |
| 663 | + patchDocument.Add(new JsonPatchOperation |
| 664 | + { |
| 665 | + Operation = Operation.Replace, |
| 666 | + Path = "/fields/System.Title", |
| 667 | + Value = title |
| 668 | + }); |
| 669 | + } |
| 670 | + |
| 671 | + if (description != null) |
| 672 | + { |
| 673 | + patchDocument.Add(new JsonPatchOperation |
| 674 | + { |
| 675 | + Operation = Operation.Replace, |
| 676 | + Path = "/fields/System.Description", |
| 677 | + Value = description |
| 678 | + }); |
| 679 | + } |
| 680 | + |
| 681 | + if (assignedTo != null) |
| 682 | + { |
| 683 | + patchDocument.Add(new JsonPatchOperation |
| 684 | + { |
| 685 | + Operation = Operation.Replace, |
| 686 | + Path = "/fields/System.AssignedTo", |
| 687 | + Value = assignedTo |
| 688 | + }); |
| 689 | + } |
| 690 | + |
| 691 | + if (state != null) |
| 692 | + { |
| 693 | + patchDocument.Add(new JsonPatchOperation |
| 694 | + { |
| 695 | + Operation = Operation.Replace, |
| 696 | + Path = "/fields/System.State", |
| 697 | + Value = state |
| 698 | + }); |
| 699 | + } |
| 700 | + |
| 701 | + if (areaPath != null) |
| 702 | + { |
| 703 | + patchDocument.Add(new JsonPatchOperation |
| 704 | + { |
| 705 | + Operation = Operation.Replace, |
| 706 | + Path = "/fields/System.AreaPath", |
| 707 | + Value = areaPath |
| 708 | + }); |
| 709 | + } |
| 710 | + |
| 711 | + if (iterationPath != null) |
| 712 | + { |
| 713 | + patchDocument.Add(new JsonPatchOperation |
| 714 | + { |
| 715 | + Operation = Operation.Replace, |
| 716 | + Path = "/fields/System.IterationPath", |
| 717 | + Value = iterationPath |
| 718 | + }); |
| 719 | + } |
| 720 | + |
| 721 | + if (priority.HasValue) |
| 722 | + { |
| 723 | + patchDocument.Add(new JsonPatchOperation |
| 724 | + { |
| 725 | + Operation = Operation.Replace, |
| 726 | + Path = "/fields/Microsoft.VSTS.Common.Priority", |
| 727 | + Value = priority.Value |
| 728 | + }); |
| 729 | + } |
| 730 | + |
| 731 | + if (tags != null) |
| 732 | + { |
| 733 | + patchDocument.Add(new JsonPatchOperation |
| 734 | + { |
| 735 | + Operation = Operation.Replace, |
| 736 | + Path = "/fields/System.Tags", |
| 737 | + Value = tags |
| 738 | + }); |
| 739 | + } |
| 740 | + |
| 741 | + if (additionalFields != null) |
| 742 | + { |
| 743 | + foreach (var field in additionalFields) |
| 744 | + { |
| 745 | + var path = field.Key.StartsWith("/fields/") |
| 746 | + ? field.Key |
| 747 | + : $"/fields/{field.Key}"; |
| 748 | + |
| 749 | + patchDocument.Add(new JsonPatchOperation |
| 750 | + { |
| 751 | + Operation = Operation.Replace, |
| 752 | + Path = path, |
| 753 | + Value = field.Value |
| 754 | + }); |
| 755 | + } |
| 756 | + } |
| 757 | + |
| 758 | + // If no fields to update, just return the current work item |
| 759 | + if (patchDocument.Count == 0) |
| 760 | + { |
| 761 | + var currentWorkItem = await GetWorkItemAsync(workItemId, project, cancellationToken); |
| 762 | + return currentWorkItem!; |
| 763 | + } |
| 764 | + |
| 765 | + var result = await _witClient.UpdateWorkItemAsync( |
| 766 | + document: patchDocument, |
| 767 | + id: workItemId, |
| 768 | + project: project ?? _options.DefaultProject, |
| 769 | + cancellationToken: cancellationToken); |
| 770 | + |
| 771 | + return MapToDto(result, includeAllFields: true); |
| 772 | + } |
| 773 | + catch (Exception ex) |
| 774 | + { |
| 775 | + _logger.LogError(ex, "Error updating work item {WorkItemId}", workItemId); |
| 776 | + throw; |
| 777 | + } |
| 778 | + } |
| 779 | + |
494 | 780 | #region Git Operations |
495 | 781 |
|
496 | 782 | public async Task<IReadOnlyList<RepositoryDto>> GetRepositoriesAsync(string? project = null, CancellationToken cancellationToken = default) |
|
0 commit comments