github-repos-manager-mcp
Version:
Model Context Protocol (MCP) server that enables your MCP client (e.g., Claude Desktop, Cline, Roo Code, Cursor, Windsurf, etc.) to interact with GitHub repositories using your GitHub personal access token.
1,091 lines (1,015 loc) • 38.5 kB
JavaScript
// Import all handler modules
const GitHubAPIService = require("./services/github-api.cjs");
const repositoryHandlerFunctions = require("./handlers/repository.cjs");
const issueHandlerFunctions = require("./handlers/issues.cjs");
const commentHandlerFunctions = require("./handlers/comments.cjs");
const pullRequestHandlerFunctions = require("./handlers/pull-requests.cjs");
const userHandlerFunctions = require("./handlers/users.cjs");
const labelsHandlerFunctions = require("./handlers/labels.cjs");
const milestonesHandlerFunctions = require("./handlers/milestones.cjs");
const branchCommitHandlerFunctions = require("./handlers/branches-commits.cjs");
const EnhancedPullRequestHandlersModule = require("./handlers/enhanced-pull-requests.cjs");
const FileManagementHandlersModule = require("./handlers/file-management.cjs");
const SecurityHandlersModule = require("./handlers/security.cjs");
const AnalyticsHandlersModule = require("./handlers/analytics.cjs");
const SearchHandlersModule = require("./handlers/search.cjs");
const OrganizationHandlersModule = require("./handlers/organizations.cjs");
const ProjectsHandlersModule = require("./handlers/projects.cjs");
const WorkflowsHandlersModule = require("./handlers/workflows.cjs");
const AdvancedFeaturesHandlerModule = require("./handlers/advanced-features.cjs");
const toolsConfig = require("./utils/tools-config.cjs");
const { formatHandlerResponse } = require("./utils/response-formatter.cjs");
const { handleError } = require("./utils/error-handler.cjs");
class GitHubMCPServer {
constructor(config = {}, sdkModules = {}) {
this.Server = sdkModules.Server;
this.StdioServerTransport = sdkModules.StdioServerTransport;
this.CallToolRequestSchema = sdkModules.CallToolRequestSchema;
this.ListToolsRequestSchema = sdkModules.ListToolsRequestSchema;
this.defaultOwner = null;
this.defaultRepo = null;
this.disabledTools = new Set();
this.allowedTools = null; // null means all tools allowed, Set means only specific tools
this.allowedRepos = null; // null means all repos allowed, Set means only specific repos/owners
const token = process.env.GH_TOKEN
? process.env.GH_TOKEN.trim()
: undefined;
this.api = new GitHubAPIService(token);
this.enhancedPrHandler = EnhancedPullRequestHandlersModule;
this.fileManagementHandler = FileManagementHandlersModule;
this.securityHandler = SecurityHandlersModule;
this.analyticsHandler = AnalyticsHandlersModule;
this.searchHandler = SearchHandlersModule;
this.organizationHandler = OrganizationHandlersModule;
this.projectsHandler = ProjectsHandlersModule;
this.workflowsHandler = WorkflowsHandlersModule;
this.advancedFeaturesHandler = AdvancedFeaturesHandlerModule;
// Set default repository if provided
this.defaultOwner = config.defaultOwner || process.env.GH_DEFAULT_OWNER;
this.defaultRepo = config.defaultRepo || process.env.GH_DEFAULT_REPO;
// Handle disabled tools from config or environment
const disabledToolsFromEnv = process.env.GH_DISABLED_TOOLS;
const disabledToolsFromConfig = config.disabledTools;
if (disabledToolsFromEnv || disabledToolsFromConfig) {
// If config.disabledTools is already an array, use it directly
if (Array.isArray(disabledToolsFromConfig)) {
disabledToolsFromConfig.forEach((toolName) =>
this.disabledTools.add(toolName.trim())
);
} else if (disabledToolsFromEnv) {
// Parse from environment variable as comma-separated string
disabledToolsFromEnv
.split(",")
.forEach((toolName) => this.disabledTools.add(toolName.trim()));
}
}
// Handle allowed tools from config or environment
const allowedToolsFromEnv = process.env.GH_ALLOWED_TOOLS;
const allowedToolsFromConfig = config.allowedTools;
if (allowedToolsFromEnv || allowedToolsFromConfig) {
this.allowedTools = new Set();
// If config.allowedTools is already an array, use it directly
if (Array.isArray(allowedToolsFromConfig)) {
allowedToolsFromConfig.forEach((toolName) =>
this.allowedTools.add(toolName.trim())
);
} else if (allowedToolsFromEnv) {
// Parse from environment variable as comma-separated string
allowedToolsFromEnv
.split(",")
.forEach((toolName) => this.allowedTools.add(toolName.trim()));
}
}
// Handle allowed repos from config or environment
const allowedReposFromEnv = process.env.GH_ALLOWED_REPOS;
const allowedReposFromConfig = config.allowedRepos;
if (allowedReposFromEnv || allowedReposFromConfig) {
this.allowedRepos = new Set();
// If config.allowedRepos is already an array, use it directly
if (Array.isArray(allowedReposFromConfig)) {
allowedReposFromConfig.forEach((repo) =>
this.allowedRepos.add(repo.trim())
);
} else if (allowedReposFromEnv) {
// Parse from environment variable as comma-separated string
allowedReposFromEnv
.split(",")
.forEach((repo) => this.allowedRepos.add(repo.trim()));
}
}
this.server = new this.Server(
{
name: "GitHub Repos Manager MCP Server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
}
setDefaultRepo(owner, repo) {
// First check if this repo is allowed
if (this.allowedRepos) {
const repoIdentifier = `${owner}/${repo}`;
const isAllowed =
this.allowedRepos.has(repoIdentifier) ||
this.allowedRepos.has(owner) ||
Array.from(this.allowedRepos).some((allowed) => {
if (allowed.includes("/")) {
return allowed === repoIdentifier;
}
return allowed === owner;
});
if (!isAllowed) {
const allowedList = Array.from(this.allowedRepos).join(", ");
return {
content: [
{
type: "text",
text: `Repository ${repoIdentifier} is not in the allowed repositories list. Allowed: ${allowedList}`,
},
],
isError: true,
};
}
}
this.defaultOwner = owner;
this.defaultRepo = repo;
const successMessage = `📦 Default repository set to: ${owner}/${repo}`;
console.error(successMessage);
return {
content: [{ type: "text", text: successMessage }],
};
}
getHandlerArgs(args) {
const effectiveArgs = { ...(args || {}) };
// Apply defaults if not specified
if (!effectiveArgs.owner && this.defaultOwner) {
effectiveArgs.owner = this.defaultOwner;
}
if (!effectiveArgs.repo && this.defaultRepo) {
effectiveArgs.repo = this.defaultRepo;
}
// Check if the repository is allowed
if (this.allowedRepos && effectiveArgs.owner) {
let isAllowed = false;
if (effectiveArgs.repo) {
// Check full repo path
const repoIdentifier = `${effectiveArgs.owner}/${effectiveArgs.repo}`;
isAllowed =
this.allowedRepos.has(repoIdentifier) ||
this.allowedRepos.has(effectiveArgs.owner);
} else {
// Only owner specified, check if owner is allowed
isAllowed =
this.allowedRepos.has(effectiveArgs.owner) ||
Array.from(this.allowedRepos).some((allowed) =>
allowed.startsWith(`${effectiveArgs.owner}/`)
);
}
if (!isAllowed) {
const allowedList = Array.from(this.allowedRepos).join(", ");
const attempted = effectiveArgs.repo
? `${effectiveArgs.owner}/${effectiveArgs.repo}`
: effectiveArgs.owner;
throw new Error(
`Repository or owner '${attempted}' is not in the allowed list. Allowed: ${allowedList}`
);
}
}
return effectiveArgs;
}
setupToolHandlers() {
this.server.setRequestHandler(this.ListToolsRequestSchema, async () => {
try {
const allTools = Object.values(toolsConfig);
let availableTools;
if (this.allowedTools) {
// If allowedTools is set, only those tools are available (disabledTools is ignored)
availableTools = allTools.filter((tool) =>
this.allowedTools.has(tool.name)
);
} else {
// If allowedTools is not set, all tools except disabled ones are available
availableTools = allTools.filter(
(tool) => !this.disabledTools.has(tool.name)
);
}
return {
tools: availableTools,
};
} catch (error) {
const errorResponse = handleError(error, "list_tools");
return {
tools: [],
error: errorResponse.message,
};
}
});
this.server.setRequestHandler(
this.CallToolRequestSchema,
async (request) => {
let processedArgs;
let name, rawArgs;
try {
// Validate request structure
if (!request || typeof request !== "object") {
throw new Error("Invalid request: request must be an object");
}
if (!request.params || typeof request.params !== "object") {
throw new Error("Invalid request: missing or invalid params");
}
name = request.params.name;
rawArgs = request.params.arguments;
// Validate tool name
if (!name || typeof name !== "string" || name.trim() === "") {
throw new Error(
"Invalid request: tool name is required and must be a non-empty string"
);
}
name = name.trim();
// Ensure rawArgs is an object (or null)
if (
rawArgs !== null &&
rawArgs !== undefined &&
typeof rawArgs !== "object"
) {
throw new Error(
"Invalid request: arguments must be an object, null, or undefined"
);
}
// Default to empty object if args are not provided
rawArgs = rawArgs || {};
// Check if tool is allowed based on configuration
if (this.allowedTools) {
// If allowedTools is set, only those tools can be used
if (!this.allowedTools.has(name)) {
throw new Error(
`Tool '${name}' is not in the allowed tools list.`
);
}
} else {
// If allowedTools is not set, check disabled tools
if (this.disabledTools.has(name)) {
throw new Error(
`Tool '${name}' is disabled by server configuration.`
);
}
}
// For 'set_default_repo', we don't want to process args through getHandlerArgs
// as it might throw an error if the repo is locked and args are different.
// The setDefaultRepo method itself handles the locking logic.
if (name === "set_default_repo") {
if (!rawArgs || !rawArgs.owner || !rawArgs.repo) {
throw new Error(
"Both owner and repo are required in arguments for set_default_repo"
);
}
return this.setDefaultRepo(rawArgs.owner, rawArgs.repo);
}
// Validate API service is available
if (!this.api) {
throw new Error("GitHub API service is not initialized");
}
// For all other tools, process arguments to apply default/locked repo context
processedArgs = this.getHandlerArgs(rawArgs);
switch (name) {
// Repository tools
case "list_repos":
return await repositoryHandlerFunctions.listRepos(
processedArgs,
this.api
);
case "get_repo_info":
return await repositoryHandlerFunctions.getRepoInfo(
processedArgs,
this.api
);
case "search_repos":
return await repositoryHandlerFunctions.searchRepos(
processedArgs,
this.api
);
case "get_repo_contents":
return await repositoryHandlerFunctions.getRepoContents(
processedArgs,
this.api
);
// set_default_repo is handled above
case "list_repo_collaborators":
return await repositoryHandlerFunctions.listRepoCollaborators(
processedArgs,
this.api
);
// Issue tools
case "list_issues":
return await issueHandlerFunctions.listIssues(
processedArgs,
this.api
);
case "create_issue":
return await issueHandlerFunctions.createIssue(
processedArgs,
this.api
);
case "edit_issue":
return await issueHandlerFunctions.editIssue(
processedArgs,
this.api
);
case "get_issue_details":
return await issueHandlerFunctions.getIssueDetails(
processedArgs,
this.api
);
case "lock_issue":
return await issueHandlerFunctions.lockIssue(
processedArgs,
this.api
);
case "unlock_issue":
return await issueHandlerFunctions.unlockIssue(
processedArgs,
this.api
);
case "add_assignees_to_issue":
return await issueHandlerFunctions.addAssigneesToIssue(
processedArgs,
this.api
);
case "remove_assignees_from_issue":
return await issueHandlerFunctions.removeAssigneesFromIssue(
processedArgs,
this.api
);
// Comment tools
case "list_issue_comments":
return await commentHandlerFunctions.listIssueComments(
processedArgs,
this.api
);
case "create_issue_comment":
return await commentHandlerFunctions.createIssueComment(
processedArgs,
this.api
);
case "edit_issue_comment":
return await commentHandlerFunctions.editIssueComment(
processedArgs,
this.api
);
case "delete_issue_comment":
return await commentHandlerFunctions.deleteIssueComment(
processedArgs,
this.api
);
// Pull request tools
case "list_prs":
return await pullRequestHandlerFunctions.listPRs(
processedArgs,
this.api
);
// Enhanced Pull Request tools
case "create_pull_request":
return formatHandlerResponse(
await this.enhancedPrHandler.create_pull_request(
processedArgs,
this.api
)
);
case "edit_pull_request":
return formatHandlerResponse(
await this.enhancedPrHandler.edit_pull_request(
processedArgs,
this.api
)
);
case "get_pr_details":
return formatHandlerResponse(
await this.enhancedPrHandler.get_pr_details(
processedArgs,
this.api
)
);
case "list_pr_reviews":
return formatHandlerResponse(
await this.enhancedPrHandler.list_pr_reviews(
processedArgs,
this.api
)
);
case "create_pr_review":
return formatHandlerResponse(
await this.enhancedPrHandler.create_pr_review(
processedArgs,
this.api
)
);
case "list_pr_files":
return formatHandlerResponse(
await this.enhancedPrHandler.list_pr_files(
processedArgs,
this.api
)
);
// File Management tools
case "create_file":
return formatHandlerResponse(
await this.fileManagementHandler.createFileHandler(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "update_file":
return formatHandlerResponse(
await this.fileManagementHandler.updateFileHandler(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "upload_file":
return formatHandlerResponse(
await this.fileManagementHandler.uploadFileHandler(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "delete_file":
return formatHandlerResponse(
await this.fileManagementHandler.deleteFileHandler(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
// User tools
case "get_user_info":
return await userHandlerFunctions.getUserInfo(
processedArgs,
this.api
);
// Labels tools
case "list_repo_labels":
return await labelsHandlerFunctions.listRepoLabels(
processedArgs,
this.api
);
case "create_label":
return await labelsHandlerFunctions.createLabel(
processedArgs,
this.api
);
case "edit_label":
return await labelsHandlerFunctions.editLabel(
processedArgs,
this.api
);
case "delete_label":
return await labelsHandlerFunctions.deleteLabel(
processedArgs,
this.api
);
// Milestones tools
case "list_milestones":
return await milestonesHandlerFunctions.listMilestones(
processedArgs,
this.api
);
case "create_milestone":
return await milestonesHandlerFunctions.createMilestone(
processedArgs,
this.api
);
case "edit_milestone":
return await milestonesHandlerFunctions.editMilestone(
processedArgs,
this.api
);
case "delete_milestone":
return await milestonesHandlerFunctions.deleteMilestone(
processedArgs,
this.api
);
// Branch and Commit tools
case "list_branches":
return await branchCommitHandlerFunctions.listBranches(
processedArgs,
this.api
);
case "create_branch":
return await branchCommitHandlerFunctions.createBranch(
processedArgs,
this.api
);
case "list_commits":
return await branchCommitHandlerFunctions.listCommits(
processedArgs,
this.api
);
case "get_commit_details":
return await branchCommitHandlerFunctions.getCommitDetails(
processedArgs,
this.api
);
case "compare_commits":
return await branchCommitHandlerFunctions.compareCommits(
processedArgs,
this.api
);
// Security & Access Management tools
case "list_deploy_keys":
return formatHandlerResponse(
await this.securityHandler.list_deploy_keys(
processedArgs,
this.api
)
);
case "create_deploy_key":
return formatHandlerResponse(
await this.securityHandler.create_deploy_key(
processedArgs,
this.api
)
);
case "delete_deploy_key":
return formatHandlerResponse(
await this.securityHandler.delete_deploy_key(
processedArgs,
this.api
)
);
case "list_webhooks":
return formatHandlerResponse(
await this.securityHandler.list_webhooks(
processedArgs,
this.api
)
);
case "create_webhook":
return formatHandlerResponse(
await this.securityHandler.create_webhook(
processedArgs,
this.api
)
);
case "edit_webhook":
return formatHandlerResponse(
await this.securityHandler.edit_webhook(processedArgs, this.api)
);
case "delete_webhook":
return formatHandlerResponse(
await this.securityHandler.delete_webhook(
processedArgs,
this.api
)
);
case "list_secrets":
return formatHandlerResponse(
await this.securityHandler.list_secrets(processedArgs, this.api)
);
case "update_secret":
return formatHandlerResponse(
await this.securityHandler.update_secret(
processedArgs,
this.api
)
);
// Repository Analytics & Insights tools
case "get_repo_stats":
return formatHandlerResponse(
await this.analyticsHandler.get_repo_stats(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "list_repo_topics":
return formatHandlerResponse(
await this.analyticsHandler.list_repo_topics(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "update_repo_topics":
return formatHandlerResponse(
await this.analyticsHandler.update_repo_topics(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "get_repo_languages":
return formatHandlerResponse(
await this.analyticsHandler.get_repo_languages(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "list_stargazers":
return formatHandlerResponse(
await this.analyticsHandler.list_stargazers(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "list_watchers":
return formatHandlerResponse(
await this.analyticsHandler.list_watchers(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "list_forks":
return formatHandlerResponse(
await this.analyticsHandler.list_forks(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "get_repo_traffic":
return formatHandlerResponse(
await this.analyticsHandler.get_repo_traffic(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
// Search tools
case "search_issues":
return formatHandlerResponse(
await this.searchHandler.search_issues(
this.api.octokit,
{ owner: this.defaultOwner, repo: this.defaultRepo },
processedArgs,
this.api
)
);
case "search_commits":
return formatHandlerResponse(
await this.searchHandler.search_commits(
this.api.octokit,
{ owner: this.defaultOwner, repo: this.defaultRepo },
processedArgs,
this.api
)
);
case "search_code":
return formatHandlerResponse(
await this.searchHandler.search_code(
this.api.octokit,
{ owner: this.defaultOwner, repo: this.defaultRepo },
processedArgs,
this.api
)
);
case "search_users":
return formatHandlerResponse(
await this.searchHandler.search_users(
this.api.octokit,
{ owner: this.defaultOwner, repo: this.defaultRepo },
processedArgs,
this.api
)
);
case "search_topics":
return formatHandlerResponse(
await this.searchHandler.search_topics(
this.api.octokit,
{ owner: this.defaultOwner, repo: this.defaultRepo },
processedArgs,
this.api
)
);
// Organization tools
case "list_org_repos":
return formatHandlerResponse(
await this.organizationHandler.listOrgRepos(
this.api.octokit,
processedArgs,
this.api
)
);
case "list_org_members":
return formatHandlerResponse(
await this.organizationHandler.listOrgMembers(
this.api.octokit,
processedArgs,
this.api
)
);
case "get_org_info":
return formatHandlerResponse(
await this.organizationHandler.getOrgInfo(
this.api.octokit,
processedArgs,
this.api
)
);
case "list_org_teams":
return formatHandlerResponse(
await this.organizationHandler.listOrgTeams(
this.api.octokit,
processedArgs,
this.api
)
);
case "get_team_members":
return formatHandlerResponse(
await this.organizationHandler.getTeamMembers(
this.api.octokit,
processedArgs,
this.api
)
);
case "manage_team_repos":
return formatHandlerResponse(
await this.organizationHandler.manageTeamRepos(
this.api.octokit,
processedArgs,
this.api
)
);
// Projects & Boards tools
case "list_repo_projects":
return formatHandlerResponse(
await this.projectsHandler.list_repo_projects(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "create_project":
return formatHandlerResponse(
await this.projectsHandler.create_project(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "list_project_columns":
return formatHandlerResponse(
await this.projectsHandler.list_project_columns(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "list_project_cards":
return formatHandlerResponse(
await this.projectsHandler.list_project_cards(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "create_project_card":
return formatHandlerResponse(
await this.projectsHandler.create_project_card(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "move_project_card":
return formatHandlerResponse(
await this.projectsHandler.move_project_card(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
// GitHub Actions & Workflows tools
case "list_workflows":
return formatHandlerResponse(
await this.workflowsHandler.list_workflows(
processedArgs,
this.api
)
);
case "list_workflow_runs":
return formatHandlerResponse(
await this.workflowsHandler.list_workflow_runs(
processedArgs,
this.api
)
);
case "get_workflow_run_details":
return formatHandlerResponse(
await this.workflowsHandler.get_workflow_run_details(
processedArgs,
this.api
)
);
case "trigger_workflow":
return formatHandlerResponse(
await this.workflowsHandler.trigger_workflow(
processedArgs,
this.api
)
);
case "download_workflow_artifacts":
return formatHandlerResponse(
await this.workflowsHandler.download_workflow_artifacts(
processedArgs,
this.api
)
);
case "cancel_workflow_run":
return formatHandlerResponse(
await this.workflowsHandler.cancel_workflow_run(
processedArgs,
this.api
)
);
// Advanced Features tools
case "code_quality_checks":
return formatHandlerResponse(
await this.advancedFeaturesHandler.handleCodeQualityChecks(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "custom_dashboards":
return formatHandlerResponse(
await this.advancedFeaturesHandler.handleCustomDashboards(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "automated_reporting":
return formatHandlerResponse(
await this.advancedFeaturesHandler.handleAutomatedReporting(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "notification_management":
return formatHandlerResponse(
await this.advancedFeaturesHandler.handleNotificationManagement(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "release_management":
return formatHandlerResponse(
await this.advancedFeaturesHandler.handleReleaseManagement(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
case "dependency_analysis":
return formatHandlerResponse(
await this.advancedFeaturesHandler.handleDependencyAnalysis(
processedArgs,
{ owner: this.defaultOwner, repo: this.defaultRepo },
this.api
)
);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
// Ensure we always have a safe fallback error message
let errorMessage = "An unexpected error occurred";
let toolName = name || "unknown_tool";
try {
// Use enhanced error handling
const errorResponse = handleError(error, `tool:${toolName}`, {
toolName: toolName,
arguments: rawArgs,
processedArgs: processedArgs,
});
errorMessage = errorResponse.message;
} catch (handlerError) {
// If even the error handler fails, use a simple message
errorMessage = `Error in ${toolName}: ${
error?.message || "Unknown error occurred"
}`;
// Log to stderr to avoid MCP protocol interference
process.stderr.write(
`Error handler failed: ${handlerError.message}\n`
);
process.stderr.write(
`Original error: ${error?.message || "Unknown"}\n`
);
}
return {
content: [
{
type: "text",
text: errorMessage,
},
],
isError: true,
};
}
}
);
}
async run() {
let authenticatedUser;
try {
authenticatedUser = await this.api.testAuthentication();
} catch (error) {
console.error("Failed to authenticate with GitHub:", error.message);
process.exit(1);
}
const transport = new this.StdioServerTransport();
try {
await this.server.connect(transport);
console.error("🚀 GitHub Repos Manager MCP Server is running...");
console.error(
`👤 Authenticated as: ${authenticatedUser.login} (${
authenticatedUser.name || "No name set"
}) ${
authenticatedUser.email ? "email: " + authenticatedUser.email : ""
}`
);
const totalToolsCount = Object.keys(toolsConfig).length;
let availableToolsCount;
// Show default repository if set
if (this.defaultOwner && this.defaultRepo) {
console.error(
`📦 Default repository: ${this.defaultOwner}/${this.defaultRepo}`
);
}
// Show allowed repositories if restricted
if (this.allowedRepos) {
const allowedReposList = Array.from(this.allowedRepos).join(", ");
console.error(
`🔒 Repository operations restricted to: ${allowedReposList}`
);
}
if (this.allowedTools) {
// When allowedTools is set, only those tools are available
availableToolsCount = this.allowedTools.size;
const allowedToolNames = Array.from(this.allowedTools).join(", ");
console.error(
`${availableToolsCount} out of ${totalToolsCount} tools are available (allowed tools only).`
);
console.error(
`✅ Allowed tools (${availableToolsCount}): ${allowedToolNames}`
);
} else {
// When allowedTools is not set, all tools except disabled ones are available
const disabledToolsCount = this.disabledTools.size;
availableToolsCount = totalToolsCount - disabledToolsCount;
if (disabledToolsCount === 0) {
console.error(`✅ All ${totalToolsCount} tools are available.`);
} else {
console.error(
`✅ ${availableToolsCount} out of ${totalToolsCount} tools are available.`
);
const disabledToolNames = Array.from(this.disabledTools).join(", ");
console.error(
`❌ Disabled tools (${disabledToolsCount}): ${disabledToolNames}`
);
}
}
process.stdin.resume();
} catch (error) {
console.error("Failed to connect server:", error);
process.exit(1);
}
}
}
// Global error handlers to prevent server crashes
process.on("uncaughtException", (error) => {
process.stderr.write(`Uncaught Exception: ${error.message}\n`);
process.stderr.write(`Stack: ${error.stack}\n`);
// Don't exit on uncaught exceptions - try to continue running
});
process.on("unhandledRejection", (reason, promise) => {
process.stderr.write(
`Unhandled Rejection at: ${promise}, reason: ${reason}\n`
);
// Don't exit on unhandled rejections - try to continue running
});
process.on("SIGINT", async () => {
console.error("🛑 Shutting down GitHub Repos Manager MCP Server...");
process.exit(0);
});
process.on("SIGTERM", async () => {
console.error(
"⚠️ Received SIGTERM, shutting down GitHub Repos Manager MCP Server..."
);
process.exit(0);
});
module.exports = GitHubMCPServer;