Skip to content

Commit 291c520

Browse files
viamusclaude
andcommitted
feat: add create_work_item and update_work_item MCP tools
Add two new write operations for Azure DevOps work items: - create_work_item: creates work items with all standard fields, parent linking, tags, and custom fields via additionalFields JSON - update_work_item: partial updates with null-means-no-change semantics Includes service layer implementation using JsonPatchDocument, 15 unit tests, and updated documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 97d2c6f commit 291c520

6 files changed

Lines changed: 707 additions & 1 deletion

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ src/Viamus.Azure.Devops.Mcp.Server/
299299
│ ├── IAzureDevOpsService.cs # Service interface
300300
│ └── AzureDevOpsService.cs # Implementation
301301
├── Tools/
302-
│ ├── WorkItemTools.cs # Work Item tools (9)
302+
│ ├── WorkItemTools.cs # Work Item tools (11)
303303
│ ├── GitTools.cs # Git Repository tools (6)
304304
│ ├── PullRequestTools.cs # Pull Request tools (5)
305305
│ └── PipelineTools.cs # Pipeline/Build tools (9)

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ This project implements an MCP server that exposes tools for querying and managi
114114
| `get_recent_work_items` | Gets recently changed work items |
115115
| `search_work_items` | Searches work items by title text |
116116
| `add_work_item_comment` | Adds a comment to a specific work item |
117+
| `create_work_item` | Creates a new work item (Bug, Task, User Story, etc.) with support for all standard fields, parent linking, and custom fields |
118+
| `update_work_item` | Updates an existing work item. Only specified fields are changed; omitted fields remain unchanged |
117119

118120
### Git Repository Tools
119121

@@ -377,6 +379,10 @@ After configuring the MCP client, you can ask questions like:
377379
- "What work items were changed in the last 7 days?"
378380
- "Search for work items with 'login' in the title"
379381
- "Add a comment to work item #1234 saying the bug was fixed"
382+
- "Create a new Bug in project X titled 'Login page crashes on submit'"
383+
- "Create a User Story assigned to John with priority 2 under parent #100"
384+
- "Update work item #1234 to change state to 'Resolved' and assign to Jane"
385+
- "Set the iteration path of work item #567 to 'Project\Sprint 3'"
380386

381387
### Git Repositories
382388

src/Viamus.Azure.Devops.Mcp.Server/Services/AzureDevOpsService.cs

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
66
using Microsoft.VisualStudio.Services.Common;
77
using Microsoft.VisualStudio.Services.WebApi;
8+
using Microsoft.VisualStudio.Services.WebApi.Patch;
9+
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;
810
using Viamus.Azure.Devops.Mcp.Server.Configuration;
911
using Viamus.Azure.Devops.Mcp.Server.Models;
1012

@@ -491,6 +493,290 @@ public async Task<WorkItemCommentDto> AddWorkItemCommentAsync(
491493
}
492494
}
493495

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+
494780
#region Git Operations
495781

496782
public async Task<IReadOnlyList<RepositoryDto>> GetRepositoriesAsync(string? project = null, CancellationToken cancellationToken = default)

src/Viamus.Azure.Devops.Mcp.Server/Services/IAzureDevOpsService.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,68 @@ Task<WorkItemCommentDto> AddWorkItemCommentAsync(
7575
string? project = null,
7676
CancellationToken cancellationToken = default);
7777

78+
/// <summary>
79+
/// Creates a new work item in the specified project.
80+
/// </summary>
81+
/// <param name="project">The project name.</param>
82+
/// <param name="workItemType">The work item type (e.g., Bug, Task, User Story).</param>
83+
/// <param name="title">The work item title.</param>
84+
/// <param name="description">Optional description.</param>
85+
/// <param name="assignedTo">Optional user to assign to.</param>
86+
/// <param name="areaPath">Optional area path.</param>
87+
/// <param name="iterationPath">Optional iteration path.</param>
88+
/// <param name="state">Optional initial state.</param>
89+
/// <param name="priority">Optional priority (1-4).</param>
90+
/// <param name="parentId">Optional parent work item ID to link as child.</param>
91+
/// <param name="tags">Optional semicolon-separated tags.</param>
92+
/// <param name="additionalFields">Optional dictionary of additional field reference names and values.</param>
93+
/// <param name="cancellationToken">Cancellation token.</param>
94+
/// <returns>The created work item.</returns>
95+
Task<WorkItemDto> CreateWorkItemAsync(
96+
string project,
97+
string workItemType,
98+
string title,
99+
string? description = null,
100+
string? assignedTo = null,
101+
string? areaPath = null,
102+
string? iterationPath = null,
103+
string? state = null,
104+
int? priority = null,
105+
int? parentId = null,
106+
string? tags = null,
107+
Dictionary<string, string>? additionalFields = null,
108+
CancellationToken cancellationToken = default);
109+
110+
/// <summary>
111+
/// Updates an existing work item.
112+
/// </summary>
113+
/// <param name="workItemId">The work item ID to update.</param>
114+
/// <param name="title">Optional new title.</param>
115+
/// <param name="description">Optional new description.</param>
116+
/// <param name="assignedTo">Optional new assignee.</param>
117+
/// <param name="state">Optional new state.</param>
118+
/// <param name="areaPath">Optional new area path.</param>
119+
/// <param name="iterationPath">Optional new iteration path.</param>
120+
/// <param name="priority">Optional new priority (1-4).</param>
121+
/// <param name="tags">Optional new tags.</param>
122+
/// <param name="additionalFields">Optional dictionary of additional field reference names and values.</param>
123+
/// <param name="project">The project name (optional if default project is configured).</param>
124+
/// <param name="cancellationToken">Cancellation token.</param>
125+
/// <returns>The updated work item.</returns>
126+
Task<WorkItemDto> UpdateWorkItemAsync(
127+
int workItemId,
128+
string? title = null,
129+
string? description = null,
130+
string? assignedTo = null,
131+
string? state = null,
132+
string? areaPath = null,
133+
string? iterationPath = null,
134+
int? priority = null,
135+
string? tags = null,
136+
Dictionary<string, string>? additionalFields = null,
137+
string? project = null,
138+
CancellationToken cancellationToken = default);
139+
78140
#region Git Operations
79141

80142
/// <summary>

0 commit comments

Comments
 (0)