#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { Octokit } from "@octokit/rest";
class GitHubServer {
private server: Server;
private octokit: Octokit;
constructor() {
this.server = new Server(
{ name: "github-mcp-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
const token = process.env.GITHUB_TOKEN;
if (!token) {
console.error("GITHUB_TOKEN environment variable is required");
process.exit(1);
}
this.octokit = new Octokit({ auth: token });
this.setupHandlers();
this.setupErrorHandling();
}
private setupErrorHandling(): void {
this.server.onerror = (error) => console.error("[MCP Error]", error);
process.on("SIGINT", async () => {
await this.server.close();
process.exit(0);
});
}
private setupHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: this.getTools(),
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) =>
this.handleToolCall(request.params.name, request.params.arguments ?? {})
);
}
private getTools(): Tool[] {
return [
{
name: "list_repos",
description: "List repositories for the authenticated user or an organization",
inputSchema: {
type: "object",
properties: {
type: { type: "string", enum: ["owner", "public", "private", "member"], default: "owner" },
sort: { type: "string", enum: ["created", "updated", "pushed", "full_name"], default: "updated" },
per_page: { type: "number", default: 30 },
page: { type: "number", default: 1 },
},
},
},
{
name: "get_repo",
description: "Get detailed information about a repository",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
},
required: ["owner", "repo"],
},
},
{
name: "create_repo",
description: "Create a new repository",
inputSchema: {
type: "object",
properties: {
name: { type: "string" },
description: { type: "string" },
private: { type: "boolean", default: false },
auto_init: { type: "boolean", default: false },
},
required: ["name"],
},
},
{
name: "list_issues",
description: "List issues for a repository",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
state: { type: "string", enum: ["open", "closed", "all"], default: "open" },
labels: { type: "string" },
sort: { type: "string", enum: ["created", "updated", "comments"], default: "created" },
per_page: { type: "number", default: 30 },
page: { type: "number", default: 1 },
},
required: ["owner", "repo"],
},
},
{
name: "get_issue",
description: "Get a specific issue",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
issue_number: { type: "number" },
},
required: ["owner", "repo", "issue_number"],
},
},
{
name: "create_issue",
description: "Create a new issue",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
title: { type: "string" },
body: { type: "string" },
labels: { type: "array", items: { type: "string" } },
assignees: { type: "array", items: { type: "string" } },
},
required: ["owner", "repo", "title"],
},
},
{
name: "update_issue",
description: "Update an existing issue",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
issue_number: { type: "number" },
title: { type: "string" },
body: { type: "string" },
state: { type: "string", enum: ["open", "closed"] },
},
required: ["owner", "repo", "issue_number"],
},
},
{
name: "add_issue_comment",
description: "Add a comment to an issue",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
issue_number: { type: "number" },
body: { type: "string" },
},
required: ["owner", "repo", "issue_number", "body"],
},
},
{
name: "list_pull_requests",
description: "List pull requests for a repository",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
state: { type: "string", enum: ["open", "closed", "all"], default: "open" },
sort: { type: "string", enum: ["created", "updated", "popularity", "long-running"], default: "created" },
per_page: { type: "number", default: 30 },
page: { type: "number", default: 1 },
},
required: ["owner", "repo"],
},
},
{
name: "get_pull_request",
description: "Get a specific pull request",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
pull_number: { type: "number" },
},
required: ["owner", "repo", "pull_number"],
},
},
{
name: "create_pull_request",
description: "Create a new pull request",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
title: { type: "string" },
body: { type: "string" },
head: { type: "string" },
base: { type: "string" },
draft: { type: "boolean", default: false },
},
required: ["owner", "repo", "title", "head", "base"],
},
},
{
name: "merge_pull_request",
description: "Merge a pull request",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
pull_number: { type: "number" },
commit_title: { type: "string" },
commit_message: { type: "string" },
merge_method: { type: "string", enum: ["merge", "squash", "rebase"], default: "merge" },
},
required: ["owner", "repo", "pull_number"],
},
},
{
name: "get_file_contents",
description: "Get contents of a file in a repository",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
path: { type: "string" },
ref: { type: "string", default: "main" },
},
required: ["owner", "repo", "path"],
},
},
{
name: "create_or_update_file",
description: "Create or update a file in a repository",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
path: { type: "string" },
message: { type: "string" },
content: { type: "string" },
branch: { type: "string", default: "main" },
sha: { type: "string" },
},
required: ["owner", "repo", "path", "message", "content"],
},
},
{
name: "list_branches",
description: "List branches in a repository",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
per_page: { type: "number", default: 30 },
page: { type: "number", default: 1 },
},
required: ["owner", "repo"],
},
},
{
name: "create_branch",
description: "Create a new branch",
inputSchema: {
type: "object",
properties: {
owner: { type: "string" },
repo: { type: "string" },
branch: { type: "string" },
from_branch: { type: "string", default: "main" },
},
required: ["owner", "repo", "branch"],
},
},
{
name: "search_repositories",
description: "Search for repositories",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
sort: { type: "string", enum: ["stars", "forks", "help-wanted-issues", "updated"] },
order: { type: "string", enum: ["asc", "desc"], default: "desc" },
per_page: { type: "number", default: 30 },
page: { type: "number", default: 1 },
},
required: ["query"],
},
},
{
name: "search_code",
description: "Search for code in repositories",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
sort: { type: "string", enum: ["indexed"] },
order: { type: "string", enum: ["asc", "desc"], default: "desc" },
per_page: { type: "number", default: 30 },
page: { type: "number", default: 1 },
},
required: ["query"],
},
},
{
name: "search_issues",
description: "Search for issues and pull requests",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
sort: { type: "string", enum: ["comments", "reactions", "created", "updated"] },
order: { type: "string", enum: ["asc", "desc"], default: "desc" },
per_page: { type: "number", default: 30 },
page: { type: "number", default: 1 },
},
required: ["query"],
},
},
{
name: "get_authenticated_user",
description: "Get the authenticated user's information",
inputSchema: { type: "object", properties: {} },
},
{
name: "get_user",
description: "Get information about a specific user",
inputSchema: {
type: "object",
properties: {
username: { type: "string" },
},
required: ["username"],
},
},
];
}
private async handleToolCall(name: string, args: any): Promise<any> {
try {
switch (name) {
case "list_repos": return await this.listRepos(args);
case "get_repo": return await this.getRepo(args);
case "create_repo": return await this.createRepo(args);
case "list_issues": return await this.listIssues(args);
case "get_issue": return await this.getIssue(args);
case "create_issue": return await this.createIssue(args);
case "update_issue": return await this.updateIssue(args);
case "add_issue_comment": return await this.addIssueComment(args);
case "list_pull_requests": return await this.listPullRequests(args);
case "get_pull_request": return await this.getPullRequest(args);
case "create_pull_request": return await this.createPullRequest(args);
case "merge_pull_request": return await this.mergePullRequest(args);
case "get_file_contents": return await this.getFileContents(args);
case "create_or_update_file": return await this.createOrUpdateFile(args);
case "list_branches": return await this.listBranches(args);
case "create_branch": return await this.createBranch(args);
case "search_repositories": return await this.searchRepositories(args);
case "search_code": return await this.searchCode(args);
case "search_issues": return await this.searchIssues(args);
case "get_authenticated_user": return await this.getAuthenticatedUser();
case "get_user": return await this.getUser(args);
default: throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
}
}
private async listRepos(args: any) {
const response = await this.octokit.repos.listForAuthenticatedUser({
type: args.type || "owner",
sort: args.sort || "updated",
per_page: args.per_page || 30,
page: args.page || 1,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async getRepo(args: any) {
const response = await this.octokit.repos.get({ owner: args.owner, repo: args.repo });
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async createRepo(args: any) {
const response = await this.octokit.repos.createForAuthenticatedUser({
name: args.name,
description: args.description,
private: args.private || false,
auto_init: args.auto_init || false,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async listIssues(args: any) {
const response = await this.octokit.issues.listForRepo({
owner: args.owner,
repo: args.repo,
state: args.state || "open",
labels: args.labels,
sort: args.sort || "created",
per_page: args.per_page || 30,
page: args.page || 1,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async getIssue(args: any) {
const response = await this.octokit.issues.get({
owner: args.owner,
repo: args.repo,
issue_number: args.issue_number,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async createIssue(args: any) {
const response = await this.octokit.issues.create({
owner: args.owner,
repo: args.repo,
title: args.title,
body: args.body,
labels: args.labels,
assignees: args.assignees,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async updateIssue(args: any) {
const response = await this.octokit.issues.update({
owner: args.owner,
repo: args.repo,
issue_number: args.issue_number,
title: args.title,
body: args.body,
state: args.state,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async addIssueComment(args: any) {
const response = await this.octokit.issues.createComment({
owner: args.owner,
repo: args.repo,
issue_number: args.issue_number,
body: args.body,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async listPullRequests(args: any) {
const response = await this.octokit.pulls.list({
owner: args.owner,
repo: args.repo,
state: args.state || "open",
sort: args.sort || "created",
per_page: args.per_page || 30,
page: args.page || 1,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async getPullRequest(args: any) {
const response = await this.octokit.pulls.get({
owner: args.owner,
repo: args.repo,
pull_number: args.pull_number,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async createPullRequest(args: any) {
const response = await this.octokit.pulls.create({
owner: args.owner,
repo: args.repo,
title: args.title,
body: args.body,
head: args.head,
base: args.base,
draft: args.draft || false,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async mergePullRequest(args: any) {
const response = await this.octokit.pulls.merge({
owner: args.owner,
repo: args.repo,
pull_number: args.pull_number,
commit_title: args.commit_title,
commit_message: args.commit_message,
merge_method: args.merge_method || "merge",
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async getFileContents(args: any) {
const response = await this.octokit.repos.getContent({
owner: args.owner,
repo: args.repo,
path: args.path,
ref: args.ref,
});
if (!Array.isArray(response.data) && response.data.type === "file") {
const content = Buffer.from(response.data.content, "base64").toString("utf-8");
return { content: [{ type: "text", text: JSON.stringify({ ...response.data, decoded_content: content }, null, 2) }] };
}
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async createOrUpdateFile(args: any) {
const content = Buffer.from(args.content).toString("base64");
const response = await this.octokit.repos.createOrUpdateFileContents({
owner: args.owner,
repo: args.repo,
path: args.path,
message: args.message,
content: content,
branch: args.branch,
sha: args.sha,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async listBranches(args: any) {
const response = await this.octokit.repos.listBranches({
owner: args.owner,
repo: args.repo,
per_page: args.per_page || 30,
page: args.page || 1,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async createBranch(args: any) {
const fromBranch = await this.octokit.repos.getBranch({
owner: args.owner,
repo: args.repo,
branch: args.from_branch || "main",
});
const response = await this.octokit.git.createRef({
owner: args.owner,
repo: args.repo,
ref: `refs/heads/${args.branch}`,
sha: fromBranch.data.commit.sha,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async searchRepositories(args: any) {
const response = await this.octokit.search.repos({
q: args.query,
sort: args.sort,
order: args.order || "desc",
per_page: args.per_page || 30,
page: args.page || 1,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async searchCode(args: any) {
const response = await this.octokit.search.code({
q: args.query,
sort: args.sort,
order: args.order || "desc",
per_page: args.per_page || 30,
page: args.page || 1,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async searchIssues(args: any) {
const response = await this.octokit.search.issuesAndPullRequests({
q: args.query,
sort: args.sort,
order: args.order || "desc",
per_page: args.per_page || 30,
page: args.page || 1,
});
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async getAuthenticatedUser() {
const response = await this.octokit.users.getAuthenticated();
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
private async getUser(args: any) {
const response = await this.octokit.users.getByUsername({ username: args.username });
return { content: [{ type: "text", text: JSON.stringify(response.data, null, 2) }] };
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("GitHub MCP Server running on stdio");
}
}
const server = new GitHubServer();
server.run().catch(console.error);