-
Notifications
You must be signed in to change notification settings - Fork 31
Ally remote indexing #193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: beta
Are you sure you want to change the base?
Ally remote indexing #193
Conversation
…case-functionality feat: Add updateTestCase functionality to Test Management
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces remote repository indexing functionality that enables the BrowserStack MCP Server to fetch and index files from GitHub repositories. The implementation uses Octokit to integrate with GitHub's API and provides tools for accessing remote repository content.
Key Changes
- Added GitHub repository integration using Octokit SDK for fetching remote repository files
- Implemented indexing system to cache repository structure for efficient file lookups
- Created
fetchFromRepotool and design repository context resource for accessing indexed files
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 15 comments.
Show a summary per file
| File | Description |
|---|---|
src/tools/remote-repo.ts |
Main integration point that initializes remote repository tools using environment variables |
src/tools/fetch-from-repo.ts |
Implements tool for fetching individual files from indexed repositories |
src/resources/design-repo-context.ts |
Provides resource for aggregating design-related files from the repository |
src/repo/repo-setup.ts |
Setup function that orchestrates fetcher and indexer initialization |
src/repo/RepositoryFetcher.ts |
Interface defining the contract for repository fetching operations |
src/repo/RemoteIndexer.ts |
Implements in-memory indexing of repository file structure |
src/repo/GitHubRepositoryFetcher.ts |
GitHub-specific implementation of the RepositoryFetcher interface |
src/server-factory.ts |
Registers the new remote repository tools with the MCP server |
package.json |
Adds octokit v5.0.5 dependency for GitHub API integration |
package-lock.json |
Lock file updates for octokit and its transitive dependencies |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } catch (error: any) { | ||
| return { | ||
| content: [ | ||
| { | ||
| type: "text" as const, | ||
| text: `Error fetching file: ${error.message}`, |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using any type for the error weakens type safety. Consider using error: unknown and then type-narrowing to check for .message property, or use a more specific error type.
| } catch (error: any) { | |
| return { | |
| content: [ | |
| { | |
| type: "text" as const, | |
| text: `Error fetching file: ${error.message}`, | |
| } catch (error: unknown) { | |
| let errorMessage = "Unknown error"; | |
| if ( | |
| typeof error === "object" && | |
| error !== null && | |
| "message" in error && | |
| typeof (error as { message?: unknown }).message === "string" | |
| ) { | |
| errorMessage = (error as { message: string }).message; | |
| } | |
| return { | |
| content: [ | |
| { | |
| type: "text" as const, | |
| text: `Error fetching file: ${errorMessage}`, |
| if (!githubToken || !repoOwner || !repoName) { | ||
| logger.info( | ||
| "Remote repository integration not configured. Skipping remote repo tools.", | ||
| ); | ||
| return tools; | ||
| } |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing validation for the GitHub token and repository configuration. If the token is invalid or doesn't have the necessary permissions, the error will only surface when setupRemoteRepoMCP fails. Consider validating the token format and checking basic repository access before proceeding with setup.
| const tree = await this.fetcher.getTree(); | ||
| tree.forEach((item: RepoFileMeta) => this.index.set(item.path, item)); |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The buildIndex() method lacks error handling. If fetcher.getTree() fails (e.g., due to network issues, authentication problems, or rate limiting), the error will propagate uncaught. Consider adding try-catch with appropriate error handling or documenting that callers must handle errors.
| const tree = await this.fetcher.getTree(); | |
| tree.forEach((item: RepoFileMeta) => this.index.set(item.path, item)); | |
| try { | |
| const tree = await this.fetcher.getTree(); | |
| tree.forEach((item: RepoFileMeta) => this.index.set(item.path, item)); | |
| } catch (error) { | |
| // Handle error appropriately: log and rethrow, or handle as needed | |
| console.error("Failed to build index:", error); | |
| throw error; | |
| } |
| export interface RepoFileMeta { | ||
| path: string; | ||
| sha: string; | ||
| type: "blob" | "tree"; | ||
| size?: number; | ||
| } | ||
|
|
||
| export interface RepositoryFetcher { | ||
| getTree(): Promise<RepoFileMeta[]>; // indexing | ||
| getFileContent(path: string): Promise<string>; // actual file fetching |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The RepositoryFetcher interface and RepoFileMeta interface lack documentation. Consider adding JSDoc comments to explain:
- The purpose of each method
- What each property in
RepoFileMetarepresents - When
sizemight be undefined - Expected error conditions and how they should be handled
| export interface RepoFileMeta { | |
| path: string; | |
| sha: string; | |
| type: "blob" | "tree"; | |
| size?: number; | |
| } | |
| export interface RepositoryFetcher { | |
| getTree(): Promise<RepoFileMeta[]>; // indexing | |
| getFileContent(path: string): Promise<string>; // actual file fetching | |
| /** | |
| * Metadata about a file or directory in a repository. | |
| */ | |
| export interface RepoFileMeta { | |
| /** | |
| * The path of the file or directory relative to the repository root. | |
| */ | |
| path: string; | |
| /** | |
| * The SHA hash of the file or directory. | |
| */ | |
| sha: string; | |
| /** | |
| * The type of the entry: "blob" for files, "tree" for directories. | |
| */ | |
| type: "blob" | "tree"; | |
| /** | |
| * The size of the file in bytes. May be undefined for directories ("tree") or if the size is not available. | |
| */ | |
| size?: number; | |
| } | |
| /** | |
| * Interface for fetching repository data such as file trees and file contents. | |
| */ | |
| export interface RepositoryFetcher { | |
| /** | |
| * Fetches the file tree of the repository. | |
| * @returns A promise that resolves to an array of RepoFileMeta objects representing files and directories. | |
| * @throws May reject if the repository cannot be accessed or the tree cannot be fetched. | |
| */ | |
| getTree(): Promise<RepoFileMeta[]>; | |
| /** | |
| * Fetches the content of a file at the given path. | |
| * @param path - The path to the file relative to the repository root. | |
| * @returns A promise that resolves to the file content as a string. | |
| * @throws May reject if the file does not exist, cannot be accessed, or if there is a network or permission error. | |
| */ | |
| getFileContent(path: string): Promise<string>; |
| export class RemoteIndexer { | ||
| private index: Map<string, RepoFileMeta> = new Map(); | ||
|
|
||
| constructor(private fetcher: RepositoryFetcher) {} |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The RemoteIndexer class lacks documentation. Consider adding JSDoc comments to explain:
- The purpose and lifecycle of the class
- When
buildIndex()should be called - Whether
buildIndex()can be called multiple times to refresh the index - Thread safety considerations if any
| // Set up remote repo asynchronously | ||
| setupRemoteRepoMCP({ | ||
| owner: repoOwner, | ||
| repo: repoName, | ||
| branch: repoBranch, | ||
| token: githubToken, | ||
| }) | ||
| .then(({ fetchFromRepoTool, contextResource, indexer }) => { | ||
| // Register the fetch tool | ||
| const registeredTool = server.tool( | ||
| fetchFromRepoTool.name, | ||
| fetchFromRepoTool.description, | ||
| fetchFromRepoTool.inputSchema.shape, | ||
| fetchFromRepoTool.handler, | ||
| ); | ||
|
|
||
| tools[fetchFromRepoTool.name] = registeredTool; | ||
|
|
||
| // Register the context resource | ||
| server.resource(contextResource.name, contextResource.uri, async () => | ||
| contextResource.handler(), | ||
| ); | ||
|
|
||
| logger.info( | ||
| "Remote repository tools registered successfully. Indexed %d files.", | ||
| indexer.getIndex().size, | ||
| ); | ||
| }) | ||
| .catch((error) => { | ||
| logger.error( | ||
| "Failed to set up remote repository integration: %s", | ||
| error.message, | ||
| ); | ||
| }); | ||
|
|
||
| return tools; |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The setupRemoteRepoMCP function is called asynchronously with .then()/.catch(), but the addRemoteRepoTools function returns immediately with an empty tools object. This means that when the server tries to use these tools, they won't be registered yet, leading to a race condition. The tools will only be added to the local tools object after the promise resolves, but the returned object is already passed to the caller.
Consider either:
- Making
addRemoteRepoToolsasync and awaiting the setup - Returning a promise that resolves when tools are ready
- Registering tools synchronously and deferring only the indexing operation
| recursive: "true", | ||
| }); | ||
|
|
||
| return treeRes.data.tree.map((item: any) => ({ |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using any type weakens type safety. The GitHub API's tree response has a well-defined structure. Consider using the proper Octokit types instead of any, such as RestEndpointMethodTypes["git"]["getTree"]["response"]["data"]["tree"][number].
| async getTree(): Promise<RepoFileMeta[]> { | ||
| const treeRes = await this.client.rest.git.getTree({ | ||
| owner: this.owner, | ||
| repo: this.repo, | ||
| tree_sha: this.branch, | ||
| recursive: "true", | ||
| }); | ||
|
|
||
| return treeRes.data.tree.map((item: any) => ({ | ||
| path: item.path!, | ||
| sha: item.sha!, | ||
| type: item.type as "blob" | "tree", | ||
| size: item.size, | ||
| })); |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getTree() method fetches the entire repository tree recursively, which could be very slow and memory-intensive for large repositories. Consider:
- Adding pagination support
- Implementing lazy loading or filtering at the API level
- Adding a timeout mechanism
- Documenting the performance implications in the interface comments
| async getTree(): Promise<RepoFileMeta[]> { | |
| const treeRes = await this.client.rest.git.getTree({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| tree_sha: this.branch, | |
| recursive: "true", | |
| }); | |
| return treeRes.data.tree.map((item: any) => ({ | |
| path: item.path!, | |
| sha: item.sha!, | |
| type: item.type as "blob" | "tree", | |
| size: item.size, | |
| })); | |
| /** | |
| * Fetches the entire repository tree recursively. | |
| * WARNING: For large repositories, this can be very slow and memory-intensive. | |
| * Consider using the `filter` argument to limit results, and be aware of possible timeouts. | |
| * @param filter Optional filter function to select which files to include. | |
| * @param timeoutMs Optional timeout in milliseconds (default: 10000ms). | |
| */ | |
| async getTree( | |
| filter?: (item: RepoFileMeta) => boolean, | |
| timeoutMs: number = 10000 | |
| ): Promise<RepoFileMeta[]> { | |
| // Add a timeout to the API call | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), timeoutMs); | |
| let treeRes; | |
| try { | |
| treeRes = await this.client.rest.git.getTree({ | |
| owner: this.owner, | |
| repo: this.repo, | |
| tree_sha: this.branch, | |
| recursive: "true", | |
| request: { signal: controller.signal } | |
| }); | |
| } catch (err: any) { | |
| if (err.name === "AbortError") { | |
| throw new Error(`getTree request timed out after ${timeoutMs}ms`); | |
| } | |
| throw err; | |
| } finally { | |
| clearTimeout(timeout); | |
| } | |
| let files = treeRes.data.tree.map((item: any) => ({ | |
| path: item.path!, | |
| sha: item.sha!, | |
| type: item.type as "blob" | "tree", | |
| size: item.size, | |
| })); | |
| if (filter) { | |
| files = files.filter(filter); | |
| } | |
| return files; |
| } catch (error: any) { | ||
| parts.push(`### File: ${path}\nError: ${error.message}`); |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using any type for the error weakens type safety. Consider using error: unknown and then type-narrowing to check for .message property, or use a more specific error type.
| } catch (error: any) { | |
| parts.push(`### File: ${path}\nError: ${error.message}`); | |
| } catch (error: unknown) { | |
| let errorMessage = "Unknown error"; | |
| if (typeof error === "object" && error !== null && "message" in error && typeof (error as any).message === "string") { | |
| errorMessage = (error as { message: string }).message; | |
| } | |
| parts.push(`### File: ${path}\nError: ${errorMessage}`); |
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| _config: BrowserStackConfig, |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The _config parameter is declared but unused, with an eslint-disable comment. If the config parameter is not needed for this function, consider removing it entirely rather than keeping it with a disable comment.
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |
| _config: BrowserStackConfig, |
No description provided.