Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
430 changes: 430 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"csv-parse": "^5.6.0",
"dotenv": "^16.5.0",
"form-data": "^4.0.2",
"octokit": "^5.0.5",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"sharp": "^0.34.1",
Expand Down
48 changes: 48 additions & 0 deletions src/repo/GitHubRepositoryFetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// src/repo/GitHubRepositoryFetcher.ts

import { Octokit } from "octokit";
import { RepositoryFetcher, RepoFileMeta } from "./RepositoryFetcher.js";

export class GitHubRepositoryFetcher implements RepositoryFetcher {
private client: Octokit;

constructor(
private owner: string,
private repo: string,
private branch: string,
token: string,
) {
this.client = new Octokit({ auth: token });
}

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,
}));
}

async getFileContent(path: string): Promise<string> {
const res = await this.client.rest.repos.getContent({
owner: this.owner,
repo: this.repo,
path,
ref: this.branch,
});

if (!("content" in res.data)) {
throw new Error("Not a file");
}

return Buffer.from(res.data.content, "base64").toString("utf8");
}
}
22 changes: 22 additions & 0 deletions src/repo/RemoteIndexer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// src/repo/RemoteIndexer.ts

import { RepositoryFetcher, RepoFileMeta } from "./RepositoryFetcher.js";

export class RemoteIndexer {
private index: Map<string, RepoFileMeta> = new Map();

constructor(private fetcher: RepositoryFetcher) {}

async buildIndex() {
const tree = await this.fetcher.getTree();
tree.forEach((item: RepoFileMeta) => this.index.set(item.path, item));
}

getIndex() {
return this.index;
}

fileExists(path: string) {
return this.index.has(path);
}
}
13 changes: 13 additions & 0 deletions src/repo/RepositoryFetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// src/repo/RepositoryFetcher.ts

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
}
30 changes: 30 additions & 0 deletions src/repo/repo-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// src/repo/repo-setup.ts

import { GitHubRepositoryFetcher } from "./GitHubRepositoryFetcher.js";
import { RemoteIndexer } from "./RemoteIndexer.js";
import { createFetchFromRepoTool } from "../tools/fetch-from-repo.js";
import { createDesignRepoContextResource } from "../resources/design-repo-context.js";

export interface RepoConfig {
owner: string;
repo: string;
branch: string;
token: string;
}

export async function setupRemoteRepoMCP(config: RepoConfig) {
const fetcher = new GitHubRepositoryFetcher(
config.owner,
config.repo,
config.branch,
config.token,
);

const indexer = new RemoteIndexer(fetcher);
await indexer.buildIndex(); // build repo map

const fetchFromRepoTool = createFetchFromRepoTool(indexer, fetcher);
const contextResource = createDesignRepoContextResource(indexer, fetcher);

return { fetchFromRepoTool, contextResource, indexer };
}
47 changes: 47 additions & 0 deletions src/resources/design-repo-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// src/resources/design-repo-context.ts

import { RemoteIndexer } from "../repo/RemoteIndexer.js";
import { RepositoryFetcher } from "../repo/RepositoryFetcher.js";

export function createDesignRepoContextResource(
indexer: RemoteIndexer,
fetcher: RepositoryFetcher,
) {
return {
uri: "repo://design-context",
name: "Design Repository Context",
description:
"Provides context from the remote design repository including tokens, components, and documentation",
mimeType: "text/plain",
handler: async () => {
const index = indexer.getIndex();

// Automatically select relevant files
const relevantFiles = [...index.keys()].filter((path) =>
path.match(/(tokens|design|components|docs).*\.(json|md|tsx?|css)$/),
);

const parts: string[] = [];

// Limit for safety and performance
for (const path of relevantFiles.slice(0, 10)) {
try {
const content = await fetcher.getFileContent(path);
parts.push(`### File: ${path}\n\`\`\`\n${content}\n\`\`\``);
} catch (error: any) {
parts.push(`### File: ${path}\nError: ${error.message}`);
}
}

return {
contents: [
{
uri: "repo://design-context",
mimeType: "text/plain",
text: `# Client Repository Context\n\n${parts.join("\n\n")}`,
},
],
};
},
};
}
2 changes: 2 additions & 0 deletions src/server-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import addBuildInsightsTools from "./tools/build-insights.js";
import { setupOnInitialized } from "./oninitialized.js";
import { BrowserStackConfig } from "./lib/types.js";
import addRCATools from "./tools/rca-agent.js";
import addRemoteRepoTools from "./tools/remote-repo.js";

/**
* Wrapper class for BrowserStack MCP Server
Expand Down Expand Up @@ -61,6 +62,7 @@ export class BrowserStackMcpServer {
addSelfHealTools,
addBuildInsightsTools,
addRCATools,
addRemoteRepoTools,
];

toolAdders.forEach((adder) => {
Expand Down
50 changes: 50 additions & 0 deletions src/tools/fetch-from-repo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// src/tools/fetch-from-repo.ts

import { z } from "zod";
import { RemoteIndexer } from "../repo/RemoteIndexer.js";
import { RepositoryFetcher } from "../repo/RepositoryFetcher.js";

export function createFetchFromRepoTool(
indexer: RemoteIndexer,
fetcher: RepositoryFetcher,
) {
return {
name: "fetchFromRepo",
description: "Fetch a file from a remote repository that has been indexed",
inputSchema: z.object({
path: z.string().describe("The file path in the repository"),
}),
handler: async ({ path }: { path: string }) => {
if (!indexer.fileExists(path)) {
return {
content: [
{
type: "text" as const,
text: `Error: File not found in index: ${path}`,
},
],
};
}
try {
const content = await fetcher.getFileContent(path);
return {
content: [
{
type: "text" as const,
text: `File: ${path}\n\n${content}`,
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text" as const,
text: `Error fetching file: ${error.message}`,
},
],
};
}
},
};
}
74 changes: 74 additions & 0 deletions src/tools/remote-repo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// src/tools/remote-repo.ts

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { BrowserStackConfig } from "../lib/types.js";
import { setupRemoteRepoMCP } from "../repo/repo-setup.js";
import logger from "../logger.js";

/**
* Adds remote repository tools (if configured)
*/
export default function addRemoteRepoTools(
server: McpServer,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_config: BrowserStackConfig,
): Record<string, any> {
const tools: Record<string, any> = {};

// Check if GitHub token is configured
const githubToken = process.env.GITHUB_TOKEN;
const repoOwner = process.env.GITHUB_REPO_OWNER;
const repoName = process.env.GITHUB_REPO_NAME;
const repoBranch = process.env.GITHUB_REPO_BRANCH || "main";

if (!githubToken || !repoOwner || !repoName) {
logger.info(
"Remote repository integration not configured. Skipping remote repo tools.",
);
return tools;
}

logger.info(
"Setting up remote repository integration for %s/%s (branch: %s)",
repoOwner,
repoName,
repoBranch,
);

// 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;
}
You are viewing a condensed version of this merge commit. You can view the full changes here.