Skip to content

Commit 9bee8c2

Browse files
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>
1 parent e20d514 commit 9bee8c2

File tree

4 files changed

+75
-4
lines changed

4 files changed

+75
-4
lines changed

packages/mcp/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- 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)
12+
1013
## [1.0.8] - 2025-11-10
1114

1215
### Fixed

packages/mcp/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,18 @@ Fetches code that matches the provided regex pattern in `query`.
182182

183183
### list_repos
184184

185-
Lists all repositories indexed by Sourcebot.
185+
Lists repositories indexed by Sourcebot with optional filtering and pagination.
186+
187+
<details>
188+
<summary>Parameters</summary>
189+
190+
| Name | Required | Description |
191+
|:-------------|:---------|:--------------------------------------------------------------------|
192+
| `query` | no | Filter repositories by name (case-insensitive). |
193+
| `pageNumber` | no | Page number (1-indexed, default: 1). |
194+
| `limit` | no | Number of repositories per page (default: 50). |
195+
196+
</details>
186197

187198
### get_file_source
188199

packages/mcp/src/index.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import escapeStringRegexp from 'escape-string-regexp';
77
import { z } from 'zod';
88
import { listRepos, search, getFileSource } from './client.js';
99
import { env, numberSchema } from './env.js';
10+
import { listReposRequestSchema } from './schemas.js';
1011
import { TextContent } from './types.js';
1112
import { isServiceError } from './utils.js';
1213

@@ -165,8 +166,13 @@ server.tool(
165166

166167
server.tool(
167168
"list_repos",
168-
"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.",
169-
async () => {
169+
"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.",
170+
listReposRequestSchema.shape,
171+
async ({ query, pageNumber = 1, limit = 50 }: {
172+
query?: string;
173+
pageNumber?: number;
174+
limit?: number;
175+
}) => {
170176
const response = await listRepos();
171177
if (isServiceError(response)) {
172178
return {
@@ -177,13 +183,45 @@ server.tool(
177183
};
178184
}
179185

180-
const content: TextContent[] = response.map(repo => {
186+
// Apply query filter if provided
187+
let filtered = response;
188+
if (query) {
189+
const lowerQuery = query.toLowerCase();
190+
filtered = response.filter(repo =>
191+
repo.repoName.toLowerCase().includes(lowerQuery) ||
192+
repo.repoDisplayName?.toLowerCase().includes(lowerQuery)
193+
);
194+
}
195+
196+
// Sort alphabetically for consistent pagination
197+
filtered.sort((a, b) => a.repoName.localeCompare(b.repoName));
198+
199+
// Apply pagination
200+
const startIndex = (pageNumber - 1) * limit;
201+
const endIndex = startIndex + limit;
202+
const paginated = filtered.slice(startIndex, endIndex);
203+
204+
// Format output
205+
const content: TextContent[] = paginated.map(repo => {
181206
return {
182207
type: "text",
183208
text: `id: ${repo.repoName}\nurl: ${repo.webUrl}`,
184209
}
185210
});
186211

212+
// Add pagination info
213+
if (content.length === 0 && filtered.length > 0) {
214+
content.push({
215+
type: "text",
216+
text: `No results on page ${pageNumber}. Total matching repositories: ${filtered.length}`,
217+
});
218+
} else if (filtered.length > endIndex) {
219+
content.push({
220+
type: "text",
221+
text: `Showing ${paginated.length} repositories (page ${pageNumber}). Total matching: ${filtered.length}. Use pageNumber ${pageNumber + 1} to see more.`,
222+
});
223+
}
224+
187225
return {
188226
content,
189227
};

packages/mcp/src/schemas.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,25 @@ export const repositoryQuerySchema = z.object({
156156

157157
export const listRepositoriesResponseSchema = repositoryQuerySchema.array();
158158

159+
export const listReposRequestSchema = z.object({
160+
query: z
161+
.string()
162+
.describe("Filter repositories by name or displayName (case-insensitive)")
163+
.optional(),
164+
pageNumber: z
165+
.number()
166+
.int()
167+
.positive()
168+
.describe("Page number (1-indexed, default: 1)")
169+
.default(1),
170+
limit: z
171+
.number()
172+
.int()
173+
.positive()
174+
.describe("Number of repositories per page (default: 50)")
175+
.default(50),
176+
});
177+
159178
export const fileSourceRequestSchema = z.object({
160179
fileName: z.string(),
161180
repository: z.string(),

0 commit comments

Comments
 (0)