diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index 94d37a42..72329a4d 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Added pagination and filtering to `list_repos` tool to handle large repository lists efficiently and prevent oversized responses that waste token context. [#614](https://github.com/sourcebot-dev/sourcebot/pull/614) + ## [1.0.8] - 2025-11-10 ### Fixed diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 0c64bdc3..a0a875a0 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -182,7 +182,18 @@ Fetches code that matches the provided regex pattern in `query`. ### list_repos -Lists all repositories indexed by Sourcebot. +Lists repositories indexed by Sourcebot with optional filtering and pagination. + +
+Parameters + +| Name | Required | Description | +|:-------------|:---------|:--------------------------------------------------------------------| +| `query` | no | Filter repositories by name (case-insensitive). | +| `pageNumber` | no | Page number (1-indexed, default: 1). | +| `limit` | no | Number of repositories per page (default: 50). | + +
### get_file_source diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index ab853a99..3e4750a7 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -7,6 +7,7 @@ import escapeStringRegexp from 'escape-string-regexp'; import { z } from 'zod'; import { listRepos, search, getFileSource } from './client.js'; import { env, numberSchema } from './env.js'; +import { listReposRequestSchema } from './schemas.js'; import { TextContent } from './types.js'; import { isServiceError } from './utils.js'; @@ -165,8 +166,13 @@ server.tool( server.tool( "list_repos", - "Lists all repositories in the organization. If you receive an error that indicates that you're not authenticated, please inform the user to set the SOURCEBOT_API_KEY environment variable.", - async () => { + "Lists repositories in the organization with optional filtering and pagination. If you receive an error that indicates that you're not authenticated, please inform the user to set the SOURCEBOT_API_KEY environment variable.", + listReposRequestSchema.shape, + async ({ query, pageNumber = 1, limit = 50 }: { + query?: string; + pageNumber?: number; + limit?: number; + }) => { const response = await listRepos(); if (isServiceError(response)) { return { @@ -177,13 +183,45 @@ server.tool( }; } - const content: TextContent[] = response.map(repo => { + // Apply query filter if provided + let filtered = response; + if (query) { + const lowerQuery = query.toLowerCase(); + filtered = response.filter(repo => + repo.repoName.toLowerCase().includes(lowerQuery) || + repo.repoDisplayName?.toLowerCase().includes(lowerQuery) + ); + } + + // Sort alphabetically for consistent pagination + filtered.sort((a, b) => a.repoName.localeCompare(b.repoName)); + + // Apply pagination + const startIndex = (pageNumber - 1) * limit; + const endIndex = startIndex + limit; + const paginated = filtered.slice(startIndex, endIndex); + + // Format output + const content: TextContent[] = paginated.map(repo => { return { type: "text", text: `id: ${repo.repoName}\nurl: ${repo.webUrl}`, } }); + // Add pagination info + if (content.length === 0 && filtered.length > 0) { + content.push({ + type: "text", + text: `No results on page ${pageNumber}. Total matching repositories: ${filtered.length}`, + }); + } else if (filtered.length > endIndex) { + content.push({ + type: "text", + text: `Showing ${paginated.length} repositories (page ${pageNumber}). Total matching: ${filtered.length}. Use pageNumber ${pageNumber + 1} to see more.`, + }); + } + return { content, }; diff --git a/packages/mcp/src/schemas.ts b/packages/mcp/src/schemas.ts index b477e8f1..ba46b2f1 100644 --- a/packages/mcp/src/schemas.ts +++ b/packages/mcp/src/schemas.ts @@ -156,6 +156,25 @@ export const repositoryQuerySchema = z.object({ export const listRepositoriesResponseSchema = repositoryQuerySchema.array(); +export const listReposRequestSchema = z.object({ + query: z + .string() + .describe("Filter repositories by name or displayName (case-insensitive)") + .optional(), + pageNumber: z + .number() + .int() + .positive() + .describe("Page number (1-indexed, default: 1)") + .default(1), + limit: z + .number() + .int() + .positive() + .describe("Number of repositories per page (default: 50)") + .default(50), +}); + export const fileSourceRequestSchema = z.object({ fileName: z.string(), repository: z.string(),