Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
74 changes: 50 additions & 24 deletions src/tools/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,32 @@ function configureCoreTools(
skip: z.number().optional().describe("The number of teams to skip for pagination. Defaults to 0."),
},
async ({ project, mine, top, skip }) => {
const connection = await connectionProvider();
const coreApi = await connection.getCoreApi();
const teams = await coreApi.getTeams(
project,
mine,
top,
skip,
false
);
try {
const connection = await connectionProvider();
const coreApi = await connection.getCoreApi();
const teams = await coreApi.getTeams(
project,
mine,
top,
skip,
false
);

return {
content: [{ type: "text", text: JSON.stringify(teams, null, 2) }],
};
if (!teams) {
return { content: [{ type: "text", text: "No teams found" }], isError: true };
}

return {
content: [{ type: "text", text: JSON.stringify(teams, null, 2) }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';

return {
content: [{ type: "text", text: `Error fetching project teams: ${errorMessage}` }],
isError: true
};
}
}
);

Expand All @@ -53,19 +66,32 @@ function configureCoreTools(
continuationToken: z.number().optional().describe("Continuation token for pagination. Used to fetch the next set of results if available."),
},
async ({ stateFilter, top, skip, continuationToken }) => {
const connection = await connectionProvider();
const coreApi = await connection.getCoreApi();
const projects = await coreApi.getProjects(
stateFilter,
top,
skip,
continuationToken,
false
);
try {
const connection = await connectionProvider();
const coreApi = await connection.getCoreApi();
const projects = await coreApi.getProjects(
stateFilter,
top,
skip,
continuationToken,
false
);

if (!projects) {
return { content: [{ type: "text", text: "No projects found" }], isError: true };
}

return {
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
};
return {
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';

return {
content: [{ type: "text", text: `Error fetching projects: ${errorMessage}` }],
isError: true
};
}
}
);
}
Expand Down
131 changes: 87 additions & 44 deletions src/tools/work.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,29 @@ function configureWorkTools(
timeframe: z.enum(["current"]).optional().describe("The timeframe for which to retrieve iterations. Currently, only 'current' is supported."),
},
async ({ project, team, timeframe }) => {
const connection = await connectionProvider();
const workApi = await connection.getWorkApi();
const iterations = await workApi.getTeamIterations(
{ project, team },
timeframe
);
try {
const connection = await connectionProvider();
const workApi = await connection.getWorkApi();
const iterations = await workApi.getTeamIterations(
{ project, team },
timeframe
);

if (!iterations) {
return { content: [{ type: "text", text: "No iterations found" }], isError: true };
}

return {
content: [{ type: "text", text: JSON.stringify(iterations, null, 2) }],
};
return {
content: [{ type: "text", text: JSON.stringify(iterations, null, 2) }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';

return {
content: [{ type: "text", text: `Error fetching team iterations: ${errorMessage}` }],
isError: true
};
}
}
);

Expand All @@ -53,29 +66,45 @@ function configureWorkTools(
})).describe("An array of iterations to create. Each iteration must have a name and can optionally have start and finish dates in ISO format.")
},
async ({ project, iterations }) => {
const connection = await connectionProvider();
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
try {
const connection = await connectionProvider();
const workItemTrackingApi = await connection.getWorkItemTrackingApi();
const results = [];

const results = [];
for (const { iterationName, startDate, finishDate } of iterations) {
// Step 1: Create the iteration
const iteration = await workItemTrackingApi.createOrUpdateClassificationNode(
{
name: iterationName,
attributes: {
startDate: startDate ? new Date(startDate) : undefined,
finishDate: finishDate ? new Date(finishDate) : undefined,
for (const { iterationName, startDate, finishDate } of iterations) {
// Step 1: Create the iteration
const iteration = await workItemTrackingApi.createOrUpdateClassificationNode(
{
name: iterationName,
attributes: {
startDate: startDate ? new Date(startDate) : undefined,
finishDate: finishDate ? new Date(finishDate) : undefined,
},
},
},
project,
TreeStructureGroup.Iterations
);
results.push(iteration);
}
project,
TreeStructureGroup.Iterations
);

if (iteration) {
results.push(iteration);
}
}

if (results.length === 0) {
return { content: [{ type: "text", text: "No iterations were created" }], isError: true };
}

return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';

return {
content: [{ type: "text", text: `Error creating iterations: ${errorMessage}` }],
isError: true
};
}
}
);

Expand All @@ -91,24 +120,38 @@ function configureWorkTools(
})).describe("An array of iterations to assign. Each iteration must have an identifier and a path."),
},
async ({ project, team, iterations }) => {
const connection = await connectionProvider();
const workApi = await connection.getWorkApi();
try {
const connection = await connectionProvider();
const workApi = await connection.getWorkApi();
const teamContext = { project, team };
const results = [];

for (const { identifier, path } of iterations) {
const assignment = await workApi.postTeamIteration(
{ path: path, id: identifier },
teamContext
);

const teamContext = { project, team };
const results = [];

for (const { identifier, path } of iterations) {
const assignment = await workApi.postTeamIteration(
{ path: path, id: identifier },
teamContext
);
if (assignment) {
results.push(assignment);
}
}

if (results.length === 0) {
return { content: [{ type: "text", text: "No iterations were assigned to the team" }], isError: true };
}

results.push(assignment);
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';

return {
content: [{ type: "text", text: `Error assigning iterations: ${errorMessage}` }],
isError: true
};
}

return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}
);

Expand Down
108 changes: 108 additions & 0 deletions test/src/tools/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ describe("configureCoreTools", () => {
const call = (server.tool as jest.Mock).mock.calls.find(
([toolName]) => toolName === "core_list_projects"
);

if (!call) throw new Error("core_list_projects tool not registered");
const [, , , handler] = call;

Expand Down Expand Up @@ -122,6 +123,59 @@ describe("configureCoreTools", () => {
)
);
});

it("should handle API errors correctly", async () => {
configureCoreTools(server, tokenProvider, connectionProvider);

const call = (server.tool as jest.Mock).mock.calls.find(
([toolName]) => toolName === "core_list_projects"
);

if (!call) throw new Error("core_list_projects tool not registered");
const [, , , handler] = call;

const testError = new Error("API connection failed");
(mockCoreApi.getProjects as jest.Mock).mockRejectedValue(testError);

const params = {
stateFilter: "wellFormed",
top: undefined,
skip: undefined,
continuationToken: undefined
};

const result = await handler(params);

expect(mockCoreApi.getProjects).toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Error fetching projects: API connection failed");
});

it("should handle null API results correctly", async () => {
configureCoreTools(server, tokenProvider, connectionProvider);

const call = (server.tool as jest.Mock).mock.calls.find(
([toolName]) => toolName === "core_list_projects"
);

if (!call) throw new Error("core_list_projects tool not registered");
const [, , , handler] = call;

(mockCoreApi.getProjects as jest.Mock).mockResolvedValue(null);

const params = {
stateFilter: "wellFormed",
top: undefined,
skip: undefined,
continuationToken: undefined
};

const result = await handler(params);

expect(mockCoreApi.getProjects).toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe("No projects found");
});
});

describe("list_project_teams tool", () => {
Expand All @@ -131,6 +185,7 @@ describe("configureCoreTools", () => {
const call = (server.tool as jest.Mock).mock.calls.find(
([toolName]) => toolName === "core_list_project_teams"
);

if (!call) throw new Error("core_list_project_teams tool not registered");
const [, , , handler] = call;

Expand Down Expand Up @@ -196,5 +251,58 @@ describe("configureCoreTools", () => {
)
);
});

it("should handle API errors correctly", async () => {
configureCoreTools(server, tokenProvider, connectionProvider);

const call = (server.tool as jest.Mock).mock.calls.find(
([toolName]) => toolName === "core_list_project_teams"
);

if (!call) throw new Error("core_list_project_teams tool not registered");
const [, , , handler] = call;

const testError = new Error("Team not found");
(mockCoreApi.getTeams as jest.Mock).mockRejectedValue(testError);

const params = {
project: "eb6e4656-77fc-42a1-9181-4c6d8e9da5d1",
mine: undefined,
top: undefined,
skip: undefined
};

const result = await handler(params);

expect(mockCoreApi.getTeams).toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Error fetching project teams: Team not found");
});

it("should handle null API results correctly", async () => {
configureCoreTools(server, tokenProvider, connectionProvider);

const call = (server.tool as jest.Mock).mock.calls.find(
([toolName]) => toolName === "core_list_project_teams"
);

if (!call) throw new Error("core_list_project_teams tool not registered");
const [, , , handler] = call;

(mockCoreApi.getTeams as jest.Mock).mockResolvedValue(null);

const params = {
project: "eb6e4656-77fc-42a1-9181-4c6d8e9da5d1",
mine: undefined,
top: undefined,
skip: undefined
};

const result = await handler(params);

expect(mockCoreApi.getTeams).toHaveBeenCalled();
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe("No teams found");
});
});
});
Loading