From f38b1c5dbcfe3be4d2f52a1691f75a1cb70c4f39 Mon Sep 17 00:00:00 2001 From: Nikola Pejic Date: Sun, 6 Jul 2025 15:16:54 +0000 Subject: [PATCH 1/2] List projects with name filter --- src/tools/core.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/tools/core.ts b/src/tools/core.ts index bce8943f..f14386c4 100644 --- a/src/tools/core.ts +++ b/src/tools/core.ts @@ -63,9 +63,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(); @@ -81,6 +82,19 @@ function configureCoreTools( return { content: [{ type: "text", text: "No projects found" }], isError: true }; } + // If projectNameFilter is provided, filter the projects + if (projectNameFilter) { + const filteredProjects = projects.filter(project => + project.name?.toLowerCase().includes(projectNameFilter.toLowerCase()) + ); + if (filteredProjects.length === 0) { + return { content: [{ type: "text", text: "No projects found matching the filter" }], isError: true }; + } + return { + content: [{ type: "text", text: JSON.stringify(filteredProjects, null, 2) }], + }; + } + return { content: [{ type: "text", text: JSON.stringify(projects, null, 2) }], }; From 82b113ccc5a2ff73ebb1fa9d06eb3e0f5bb9b397 Mon Sep 17 00:00:00 2001 From: Nikola Pejic Date: Sun, 6 Jul 2025 15:50:34 +0000 Subject: [PATCH 2/2] Filtering repos by name substring Also improving code formatting --- src/tools/core.ts | 30 +++++++++++++++++------------- src/tools/repos.ts | 26 ++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/tools/core.ts b/src/tools/core.ts index f14386c4..b7a57ac6 100644 --- a/src/tools/core.ts +++ b/src/tools/core.ts @@ -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, @@ -82,21 +95,12 @@ function configureCoreTools( return { content: [{ type: "text", text: "No projects found" }], isError: true }; } - // If projectNameFilter is provided, filter the projects - if (projectNameFilter) { - const filteredProjects = projects.filter(project => - project.name?.toLowerCase().includes(projectNameFilter.toLowerCase()) - ); - if (filteredProjects.length === 0) { - return { content: [{ type: "text", text: "No projects found matching the filter" }], isError: true }; - } - return { - content: [{ type: "text", text: JSON.stringify(filteredProjects, null, 2) }], - }; - } + 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'; diff --git a/src/tools/repos.ts b/src/tools/repos.ts index b85349c5..d1452eb3 100644 --- a/src/tools/repos.ts +++ b/src/tools/repos.ts @@ -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", @@ -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, @@ -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( @@ -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, @@ -162,7 +180,7 @@ function configureRepoTools( return { content: [ - { type: "text", text: JSON.stringify(filteredRepositories, null, 2) }, + { type: "text", text: JSON.stringify(trimmedRepositories, null, 2) }, ], }; }