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
24 changes: 21 additions & 3 deletions src/tools/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,24 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebApi } from "azure-devops-node-api";
import { z } from "zod";

import type { ProjectInfo } from "azure-devops-node-api/interfaces/CoreInterfaces.js";

const CORE_TOOLS = {
list_project_teams: "core_list_project_teams",
list_projects: "core_list_projects",
};

function filterProjectsByName(
projects: ProjectInfo[],
projectNameFilter: string
): ProjectInfo[]
{
const lowerCaseFilter = projectNameFilter.toLowerCase();
return projects.filter((project) =>
project.name?.toLowerCase().includes(lowerCaseFilter)
);
}

function configureCoreTools(
server: McpServer,
tokenProvider: () => Promise<AccessToken>,
Expand Down Expand Up @@ -63,9 +76,10 @@ function configureCoreTools(
stateFilter: z.enum(["all", "wellFormed", "createPending", "deleted"]).default("wellFormed").describe("Filter projects by their state. Defaults to 'wellFormed'."),
top: z.number().optional().describe("The maximum number of projects to return. Defaults to 100."),
skip: z.number().optional().describe("The number of projects to skip for pagination. Defaults to 0."),
continuationToken: z.number().optional().describe("Continuation token for pagination. Used to fetch the next set of results if available."),
continuationToken: z.number().optional().describe("Continuation token for pagination. Used to fetch the next set of results if available."),
projectNameFilter: z.string().optional().describe("Filter projects by name. Supports partial matches."),
},
async ({ stateFilter, top, skip, continuationToken }) => {
async ({ stateFilter, top, skip, continuationToken, projectNameFilter }) => {
try {
const connection = await connectionProvider();
const coreApi = await connection.getCoreApi();
Expand All @@ -81,8 +95,12 @@ function configureCoreTools(
return { content: [{ type: "text", text: "No projects found" }], isError: true };
}

const filteredProject = projectNameFilter
? filterProjectsByName(projects, projectNameFilter)
: projects;

return {
content: [{ type: "text", text: JSON.stringify(projects, null, 2) }],
content: [{ type: "text", text: JSON.stringify(filteredProject, null, 2) }],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
Expand Down
26 changes: 22 additions & 4 deletions src/tools/repos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { WebApi } from "azure-devops-node-api";
import { GitRef, PullRequestStatus } from "azure-devops-node-api/interfaces/GitInterfaces.js";
import { z } from "zod";
import { getCurrentUserDetails } from "./auth.js";
import { GitRepository } from "azure-devops-node-api/interfaces/TfvcInterfaces.js";

const REPO_TOOLS = {
list_repos_by_project: "repo_list_repos_by_project",
Expand Down Expand Up @@ -55,6 +56,18 @@ function pullRequestStatusStringToInt(
}
}

function filterReposByName(
repositories: GitRepository[],
repoNameFilter: string
): GitRepository[] {
const lowerCaseFilter = repoNameFilter.toLowerCase();
const filteredByName = repositories?.filter((repo) =>
repo.name?.toLowerCase().includes(lowerCaseFilter)
);

return filteredByName;
}

function configureRepoTools(
server: McpServer,
tokenProvider: () => Promise<AccessToken>,
Expand Down Expand Up @@ -137,9 +150,10 @@ function configureRepoTools(
REPO_TOOLS.list_repos_by_project,
"Retrieve a list of repositories for a given project",
{
project: z.string().describe("The name or ID of the Azure DevOps project."),
project: z.string().describe("The name or ID of the Azure DevOps project."),
repoNameFilter: z.string().optional().describe("Optional filter to search for repositories by name. If provided, only repositories with names containing this string will be returned."),
},
async ({ project }) => {
async ({ project, repoNameFilter }) => {
const connection = await connectionProvider();
const gitApi = await connection.getGitApi();
const repositories = await gitApi.getRepositories(
Expand All @@ -149,8 +163,12 @@ function configureRepoTools(
false
);

const filteredRepositories = repoNameFilter
? filterReposByName(repositories, repoNameFilter)
: repositories;

// Filter out the irrelevant properties
const filteredRepositories = repositories?.map((repo) => ({
const trimmedRepositories = filteredRepositories?.map((repo) => ({
id: repo.id,
name: repo.name,
isDisabled: repo.isDisabled,
Expand All @@ -162,7 +180,7 @@ function configureRepoTools(

return {
content: [
{ type: "text", text: JSON.stringify(filteredRepositories, null, 2) },
{ type: "text", text: JSON.stringify(trimmedRepositories, null, 2) },
],
};
}
Expand Down
Loading