Skip to content

Add functionality to link artifacts to work items #377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 12, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion docs/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ This guide offers step-by-step examples for using the Azure DevOps MCP Server to
[Create and Link Test Cases](#create-and-link-test-cases)<br/>
[Triage Work](#triage-work)<br/>
[Using Markdown Format](#adding-and-updating-work-items-using-the-format-paramater)<br/>
[Remove Links from a Work Item](#remove-one-or-more-links-from-a-work-item)
[Remove Links from a Work Item](#remove-one-or-more-links-from-a-work-item)<br/>
[Adding Artifact Links](#adding-artifact-links)

## πŸ™‹β€β™‚οΈ Projects and Teams

Expand Down Expand Up @@ -149,3 +150,21 @@ Next, remove a specific link to a work item, pull request, etc. or remove links
```plaintext
Remove link 5678 and 91011 from work item 1234. Also remove any related links and links to pull request 121314
```

## πŸ”— Adding Artifact Links

### Add Artifact Links to Work Items

Use this tool to link work items to repository artifacts like branches, commits, and pull requests. This is particularly useful for GitHub Copilot integration, which requires artifact links to understand repository context.

First, you'll need the proper vstfs URI format for your artifact:

- **Branch**: `vstfs:///Git/Ref/{projectId}%2F{repositoryId}%2FGB{branchName}`
- **Commit**: `vstfs:///Git/Commit/{projectId}%2F{repositoryId}%2F{commitId}`
- **Pull Request**: `vstfs:///Git/PullRequestId/{projectId}%2F{repositoryId}%2F{pullRequestId}`

```plaintext
Add a branch artifact link to work item 1234 in project "Contoso" with URI "vstfs:///Git/Ref/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2FGBmain" and link type "Branch" with comment "Linked to main branch for GitHub Copilot integration"
```

πŸ“½οΈ [Adding artifact links enables automation of work item creation and GitHub Copilot integration]()
68 changes: 68 additions & 0 deletions src/tools/workitems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const WORKITEM_TOOLS = {
update_work_items_batch: "wit_update_work_items_batch",
work_items_link: "wit_work_items_link",
work_item_unlink: "wit_work_item_unlink",
add_artifact_link: "wit_add_artifact_link",
};

function getLinkTypeFromName(name: string) {
Expand Down Expand Up @@ -853,6 +854,73 @@ function configureWorkItemTools(server: McpServer, tokenProvider: () => Promise<
}
}
);

server.tool(
WORKITEM_TOOLS.add_artifact_link,
"Add artifact links (repository, branch, commit) to work items.",
{
workItemId: z.number().describe("The ID of the work item to add the artifact link to."),
project: z.string().describe("The name or ID of the Azure DevOps project."),
artifactUri: z.string().describe("The URI of the artifact to link (e.g., vstfs:///Git/Ref/{projectId}%2F{repositoryId}%2FGB{branchName} for branches)."),
linkType: z.string().default("Branch").describe("Type of artifact link, defaults to 'Branch'."),
comment: z.string().optional().describe("Comment to include with the artifact link."),
},
async ({ workItemId, project, artifactUri, linkType, comment }) => {
try {
const connection = await connectionProvider();
const workItemTrackingApi = await connection.getWorkItemTrackingApi();

// Create the patch document for adding an artifact link relation
const patchDocument = [
{
op: "add",
path: "/relations/-",
value: {
rel: "ArtifactLink",
url: artifactUri,
attributes: {
name: linkType,
...(comment && { comment }),
},
},
},
];

// Use the WorkItem API to update the work item with the new relation
const workItem = await workItemTrackingApi.updateWorkItem({}, patchDocument, workItemId, project);

if (!workItem) {
return { content: [{ type: "text", text: "Work item update failed" }], isError: true };
}

return {
content: [
{
type: "text",
text: JSON.stringify(
{
workItemId,
artifactUri,
linkType,
comment: comment || null,
success: true,
},
null,
2
),
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";

return {
content: [{ type: "text", text: `Error adding artifact link to work item: ${errorMessage}` }],
isError: true,
};
}
}
);
}

export { WORKITEM_TOOLS, configureWorkItemTools };
116 changes: 116 additions & 0 deletions test/src/tools/workitems.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2250,4 +2250,120 @@ describe("configureWorkItemTools", () => {
expect(result.isError).toBe(true);
});
});

describe("artifact link tools", () => {
describe("wit_add_artifact_link", () => {
it("should add artifact link to work item successfully", async () => {
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);

const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link");
if (!call) throw new Error("wit_add_artifact_link tool not registered");
const [, , , handler] = call;

const mockWorkItem = { id: 1234, fields: { "System.Title": "Test Item" } };
mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue(mockWorkItem);

const params = {
workItemId: 1234,
project: "TestProject",
artifactUri: "vstfs:///Git/Ref/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2FGBmain",
linkType: "Branch",
comment: "Linked to main branch",
};

const result = await handler(params);

expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith(
{},
[
{
op: "add",
path: "/relations/-",
value: {
rel: "ArtifactLink",
url: "vstfs:///Git/Ref/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2FGBmain",
attributes: {
name: "Branch",
comment: "Linked to main branch",
},
},
},
],
1234,
"TestProject"
);

const response = JSON.parse(result.content[0].text);
expect(response.workItemId).toBe(1234);
expect(response.artifactUri).toBe("vstfs:///Git/Ref/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2FGBmain");
expect(response.linkType).toBe("Branch");
expect(response.comment).toBe("Linked to main branch");
expect(response.success).toBe(true);
});

it("should add artifact link without comment", async () => {
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);

const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link");
if (!call) throw new Error("wit_add_artifact_link tool not registered");
const [, , , handler] = call;

const mockWorkItem = { id: 1234, fields: { "System.Title": "Test Item" } };
mockWorkItemTrackingApi.updateWorkItem.mockResolvedValue(mockWorkItem);

const params = {
workItemId: 1234,
project: "TestProject",
artifactUri: "vstfs:///Git/Commit/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2Fabc123",
linkType: "Commit",
};

const result = await handler(params);

expect(mockWorkItemTrackingApi.updateWorkItem).toHaveBeenCalledWith(
{},
[
{
op: "add",
path: "/relations/-",
value: {
rel: "ArtifactLink",
url: "vstfs:///Git/Commit/12341234-1234-1234-1234-123412341234%2F12341234-1234-1234-1234-123412341234%2Fabc123",
attributes: {
name: "Commit",
},
},
},
],
1234,
"TestProject"
);

const response = JSON.parse(result.content[0].text);
expect(response.comment).toBe(null);
});

it("should handle errors when adding artifact link", async () => {
configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider);

const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wit_add_artifact_link");
if (!call) throw new Error("wit_add_artifact_link tool not registered");
const [, , , handler] = call;

mockWorkItemTrackingApi.updateWorkItem.mockRejectedValue(new Error("API Error"));

const params = {
workItemId: 1234,
project: "TestProject",
artifactUri: "vstfs:///Git/Ref/invalid",
linkType: "Branch",
};

const result = await handler(params);

expect(result.content[0].text).toBe("Error adding artifact link to work item: API Error");
expect(result.isError).toBe(true);
});
});
});
});
Loading