feat(mcp): Add pagination and filtering to list_repos tool (#614)

* feat(mcp): Add pagination and filtering to list_repos tool

Fixes #566

  - Add query parameter to filter repositories by name
  - Add pageNumber and limit parameters for pagination
  - Include pagination info in response when applicable
  - Add listReposRequestSchema for request validation
  - Update README with new list_repos parameters

* feat(mcp): Sort repositories alphabetically for consistent pagination

Fixes #566
- Updated CHANGELOG.md with pagination and filtering changes

---------

Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com>
This commit is contained in:
Teddy Reinert 2025-11-17 19:08:20 -06:00 committed by GitHub
parent e20d514569
commit 9bee8c2c59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 75 additions and 4 deletions

View file

@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ## [1.0.8] - 2025-11-10
### Fixed ### Fixed

View file

@ -182,7 +182,18 @@ Fetches code that matches the provided regex pattern in `query`.
### list_repos ### list_repos
Lists all repositories indexed by Sourcebot. Lists repositories indexed by Sourcebot with optional filtering and pagination.
<details>
<summary>Parameters</summary>
| 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). |
</details>
### get_file_source ### get_file_source

View file

@ -7,6 +7,7 @@ import escapeStringRegexp from 'escape-string-regexp';
import { z } from 'zod'; import { z } from 'zod';
import { listRepos, search, getFileSource } from './client.js'; import { listRepos, search, getFileSource } from './client.js';
import { env, numberSchema } from './env.js'; import { env, numberSchema } from './env.js';
import { listReposRequestSchema } from './schemas.js';
import { TextContent } from './types.js'; import { TextContent } from './types.js';
import { isServiceError } from './utils.js'; import { isServiceError } from './utils.js';
@ -165,8 +166,13 @@ server.tool(
server.tool( server.tool(
"list_repos", "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.", "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.",
async () => { listReposRequestSchema.shape,
async ({ query, pageNumber = 1, limit = 50 }: {
query?: string;
pageNumber?: number;
limit?: number;
}) => {
const response = await listRepos(); const response = await listRepos();
if (isServiceError(response)) { if (isServiceError(response)) {
return { 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 { return {
type: "text", type: "text",
text: `id: ${repo.repoName}\nurl: ${repo.webUrl}`, 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 { return {
content, content,
}; };

View file

@ -156,6 +156,25 @@ export const repositoryQuerySchema = z.object({
export const listRepositoriesResponseSchema = repositoryQuerySchema.array(); 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({ export const fileSourceRequestSchema = z.object({
fileName: z.string(), fileName: z.string(),
repository: z.string(), repository: z.string(),