Skip to content

Commit 9f8443f

Browse files
author
Maxim Titovich
committed
feat: Add work item link retrieval and linked work items functionality
- Implement `getWorkItemLinks` method to fetch all links associated with a work item - Add `getLinkedWorkItems` method to retrieve full details of linked work items - Introduce new server tools for accessing work item links and linked work items - Update types to include `WorkItemLink` interface for enhanced type safety - Enhance error handling for work item link extraction - Update documentation to reflect new features
1 parent e8f53d0 commit 9f8443f

File tree

4 files changed

+191
-0
lines changed

4 files changed

+191
-0
lines changed

src/azure-devops-service.ts

+101
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
PullRequestCommentRequest,
1414
PullRequestCommentResponse,
1515
PullRequestFileContent,
16+
WorkItemRelation,
17+
WorkItemLink,
1618
} from './types.js';
1719

1820
/**
@@ -297,6 +299,105 @@ class AzureDevOpsService {
297299
return attachments;
298300
}
299301

302+
/**
303+
* Get all links associated with a work item (parent, child, related, etc.)
304+
* @param workItemId The ID of the work item
305+
* @returns Object with work item links grouped by relationship type
306+
*/
307+
async getWorkItemLinks(workItemId: number): Promise<Record<string, WorkItemLink[]>> {
308+
await this.initialize();
309+
310+
if (!this.workItemClient) {
311+
throw new Error('Work item client not initialized');
312+
}
313+
314+
// Get work item with relations
315+
const workItem = await this.workItemClient.getWorkItem(
316+
workItemId,
317+
undefined,
318+
undefined,
319+
4 // 4 = WorkItemExpand.Relations in the SDK
320+
);
321+
322+
if (!workItem || !workItem.relations) {
323+
return {};
324+
}
325+
326+
// Filter for work item link relations (exclude attachments and hyperlinks)
327+
const linkRelations = workItem.relations.filter(
328+
(relation: any) => relation.rel.includes('Link') &&
329+
relation.rel !== 'AttachedFile' &&
330+
relation.rel !== 'Hyperlink'
331+
);
332+
333+
// Group relations by relationship type
334+
const groupedRelations: Record<string, WorkItemLink[]> = {};
335+
336+
linkRelations.forEach((relation: any) => {
337+
const relType = relation.rel;
338+
339+
// Extract work item ID from URL
340+
// URL format is typically like: https://dev.azure.com/{org}/{project}/_apis/wit/workItems/{id}
341+
let targetId = 0;
342+
try {
343+
const urlParts = relation.url.split('/');
344+
targetId = parseInt(urlParts[urlParts.length - 1], 10);
345+
} catch (error) {
346+
console.error('Failed to extract work item ID from URL:', relation.url);
347+
}
348+
349+
if (!groupedRelations[relType]) {
350+
groupedRelations[relType] = [];
351+
}
352+
353+
const workItemLink: WorkItemLink = {
354+
...relation,
355+
targetId,
356+
title: relation.attributes?.name || `Work Item ${targetId}`
357+
};
358+
359+
groupedRelations[relType].push(workItemLink);
360+
});
361+
362+
return groupedRelations;
363+
}
364+
365+
/**
366+
* Get all linked work items with their full details
367+
* @param workItemId The ID of the work item to get links from
368+
* @returns Array of work items that are linked to the specified work item
369+
*/
370+
async getLinkedWorkItems(workItemId: number): Promise<WorkItem[]> {
371+
await this.initialize();
372+
373+
if (!this.workItemClient) {
374+
throw new Error('Work item client not initialized');
375+
}
376+
377+
// First get all links
378+
const linkGroups = await this.getWorkItemLinks(workItemId);
379+
380+
// Extract all target IDs from all link groups
381+
const linkedIds: number[] = [];
382+
383+
Object.values(linkGroups).forEach(links => {
384+
links.forEach(link => {
385+
if (link.targetId > 0) {
386+
linkedIds.push(link.targetId);
387+
}
388+
});
389+
});
390+
391+
// If no linked items found, return empty array
392+
if (linkedIds.length === 0) {
393+
return [];
394+
}
395+
396+
// Get the full work item details for all linked items
397+
const linkedWorkItems = await this.getWorkItems(linkedIds);
398+
return linkedWorkItems;
399+
}
400+
300401
/**
301402
* Get detailed changes for a pull request
302403
*/

src/index.ts

+44
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,46 @@ server.tool(
428428
}
429429
);
430430

431+
// New tool for work item links
432+
server.tool(
433+
'azure_devops_work_item_links',
434+
'Get links for a specific work item',
435+
{
436+
id: z.number().describe('Work item ID'),
437+
},
438+
async ({ id }) => {
439+
const result = await azureDevOpsService.getWorkItemLinks(id);
440+
return {
441+
content: [
442+
{
443+
type: 'text',
444+
text: JSON.stringify(result, null, 2),
445+
},
446+
],
447+
};
448+
}
449+
);
450+
451+
// New tool for linked work items with full details
452+
server.tool(
453+
'azure_devops_linked_work_items',
454+
'Get all linked work items with their full details',
455+
{
456+
id: z.number().describe('Work item ID'),
457+
},
458+
async ({ id }) => {
459+
const result = await azureDevOpsService.getLinkedWorkItems(id);
460+
return {
461+
content: [
462+
{
463+
type: 'text',
464+
text: JSON.stringify(result, null, 2),
465+
},
466+
],
467+
};
468+
}
469+
);
470+
431471
// New tool for pull request changes with file contents
432472
server.tool(
433473
'azure_devops_pull_request_changes',
@@ -708,6 +748,10 @@ async function main() {
708748
console.error('- azure_devops_pull_request_by_id: Get a pull request by ID');
709749
console.error('- azure_devops_pull_request_threads: Get threads (comments) for a pull request');
710750
console.error('- azure_devops_work_item_attachments: Get attachments for a work item');
751+
console.error('- azure_devops_work_item_links: Get links for a work item');
752+
console.error(
753+
'- azure_devops_linked_work_items: Get all linked work items with their full details'
754+
);
711755
console.error(
712756
'- azure_devops_pull_request_changes: Get detailed code changes for a pull request'
713757
);

src/sse-server.ts

+40
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,46 @@ app.get('/sse', async (req, res) => {
236236
}
237237
);
238238

239+
// New tool for work item links
240+
server.tool(
241+
'azure_devops_work_item_links',
242+
'Get links for a specific work item',
243+
{
244+
id: z.number().describe('Work item ID'),
245+
},
246+
async ({ id }) => {
247+
const result = await azureDevOpsService.getWorkItemLinks(id);
248+
return {
249+
content: [
250+
{
251+
type: 'text',
252+
text: JSON.stringify(result, null, 2),
253+
},
254+
],
255+
};
256+
}
257+
);
258+
259+
// New tool for linked work items with full details
260+
server.tool(
261+
'azure_devops_linked_work_items',
262+
'Get all linked work items with their full details',
263+
{
264+
id: z.number().describe('Work item ID'),
265+
},
266+
async ({ id }) => {
267+
const result = await azureDevOpsService.getLinkedWorkItems(id);
268+
return {
269+
content: [
270+
{
271+
type: 'text',
272+
text: JSON.stringify(result, null, 2),
273+
},
274+
],
275+
};
276+
}
277+
);
278+
239279
// New tool for pull request changes with file contents
240280
server.tool(
241281
'azure_devops_pull_request_changes',

src/types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export interface WorkItemRelation {
3030
attributes?: Record<string, any>;
3131
}
3232

33+
// Azure DevOps Work Item Link (Extended from WorkItemRelation)
34+
export interface WorkItemLink extends WorkItemRelation {
35+
targetId: number; // The ID of the target work item
36+
title?: string; // Optional title of the target work item
37+
}
38+
3339
// Azure DevOps Git Repository
3440
export interface GitRepository {
3541
id: string;

0 commit comments

Comments
 (0)