import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { Octokit } from "@octokit/rest";
// Initialize Octokit with GitHub token from environment
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
// Tool definitions
const TOOLS = [
{
name: "github_list_repos",
description: "List repositories for a user or organization",
inputSchema: {
type: "object",
properties: {
username: {
type: "string",
description: "GitHub username or organization name (defaults to authenticated user)",
},
type: {
type: "string",
enum: ["all", "owner", "member"],
description: "Repository type filter (default: owner)",
},
sort: {
type: "string",
enum: ["created", "updated", "pushed", "full_name"],
description: "Sort order (default: updated)",
},
per_page: {
type: "number",
description: "Results per page (default: 30, max: 100)",
},
},
},
},
{
name: "github_get_repo",
description: "Get detailed information about a specific repository",
inputSchema: {
type: "object",
properties: {
owner: {
type: "string",
description: "Repository owner (username or organization)",
},
repo: {
type: "string",
description: "Repository name",
},
},
required: ["owner", "repo"],
},
},
{
name: "github_get_file",
description: "Read file contents from a repository",
inputSchema: {
type: "object",
properties: {
owner: {
type: "string",
description: "Repository owner",
},
repo: {
type: "string",
description: "Repository name",
},
path: {
type: "string",
description: "File path in the repository",
},
ref: {
type: "string",
description: "Branch, tag, or commit SHA (default: default branch)",
},
},
required: ["owner", "repo", "path"],
},
},
{
name: "github_list_issues",
description: "List issues for a repository",
inputSchema: {
type: "object",
properties: {
owner: {
type: "string",
description: "Repository owner",
},
repo: {
type: "string",
description: "Repository name",
},
state: {
type: "string",
enum: ["open", "closed", "all"],
description: "Issue state filter (default: open)",
},
labels: {
type: "string",
description: "Comma-separated list of label names",
},
per_page: {
type: "number",
description: "Results per page (default: 30, max: 100)",
},
},
required: ["owner", "repo"],
},
},
{
name: "github_search_code",
description: "Search for code across GitHub repositories",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query (e.g., 'addClass in:file language:js repo:owner/repo')",
},
per_page: {
type: "number",
description: "Results per page (default: 30, max: 100)",
},
},
required: ["query"],
},
},
{
name: "github_create_issue",
description: "Create a new issue in a repository",
inputSchema: {
type: "object",
properties: {
owner: {
type: "string",
description: "Repository owner",
},
repo: {
type: "string",
description: "Repository name",
},
title: {
type: "string",
description: "Issue title",
},
body: {
type: "string",
description: "Issue body (markdown supported)",
},
labels: {
type: "array",
items: {
type: "string",
},
description: "Labels to add to the issue",
},
assignees: {
type: "array",
items: {
type: "string",
},
description: "Usernames to assign to the issue",
},
milestone: {
type: "number",
description: "Milestone number to associate with the issue",
},
},
required: ["owner", "repo", "title"],
},
},
{
name: "github_update_issue",
description: "Update an existing issue",
inputSchema: {
type: "object",
properties: {
owner: {
type: "string",
description: "Repository owner",
},
repo: {
type: "string",
description: "Repository name",
},
issue_number: {
type: "number",
description: "Issue number",
},
title: {
type: "string",
description: "New issue title",
},
body: {
type: "string",
description: "New issue body",
},
state: {
type: "string",
enum: ["open", "closed"],
description: "Issue state",
},
labels: {
type: "array",
items: {
type: "string",
},
description: "Labels (replaces existing)",
},
assignees: {
type: "array",
items: {
type: "string",
},
description: "Assignees (replaces existing)",
},
},
required: ["owner", "repo", "issue_number"],
},
},
{
name: "github_create_comment",
description: "Add a comment to an issue or pull request",
inputSchema: {
type: "object",
properties: {
owner: {
type: "string",
description: "Repository owner",
},
repo: {
type: "string",
description: "Repository name",
},
issue_number: {
type: "number",
description: "Issue or pull request number",
},
body: {
type: "string",
description: "Comment body (markdown supported)",
},
},
required: ["owner", "repo", "issue_number", "body"],
},
},
{
name: "github_create_repo",
description: "Create a new repository for the authenticated user",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Repository name",
},
description: {
type: "string",
description: "Repository description",
},
private: {
type: "boolean",
description: "Whether the repository is private (default: false)",
},
auto_init: {
type: "boolean",
description: "Initialize with README (default: false)",
},
},
required: ["name"],
},
},
];
/**
* Register all GitHub tools on the given MCP server
*/
export function registerTools(server: Server): void {
// List tools handler
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS,
}));
// Call tool handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "github_list_repos": {
const { username, type = "owner", sort = "updated", per_page = 30 } = args as any;
let repos;
if (username) {
repos = await octokit.repos.listForUser({
username,
type,
sort,
per_page,
});
} else {
repos = await octokit.repos.listForAuthenticatedUser({
type,
sort,
per_page,
});
}
const simplified = repos.data.map((repo) => ({
name: repo.name,
full_name: repo.full_name,
description: repo.description,
private: repo.private,
html_url: repo.html_url,
language: repo.language,
stargazers_count: repo.stargazers_count,
forks_count: repo.forks_count,
updated_at: repo.updated_at,
topics: repo.topics,
}));
return {
content: [
{
type: "text",
text: JSON.stringify(simplified, null, 2),
},
],
};
}
case "github_get_repo": {
const { owner, repo } = args as any;
const response = await octokit.repos.get({ owner, repo });
const repoInfo = {
name: response.data.name,
full_name: response.data.full_name,
description: response.data.description,
private: response.data.private,
html_url: response.data.html_url,
homepage: response.data.homepage,
language: response.data.language,
stargazers_count: response.data.stargazers_count,
watchers_count: response.data.watchers_count,
forks_count: response.data.forks_count,
open_issues_count: response.data.open_issues_count,
default_branch: response.data.default_branch,
topics: response.data.topics,
created_at: response.data.created_at,
updated_at: response.data.updated_at,
pushed_at: response.data.pushed_at,
size: response.data.size,
license: response.data.license?.name,
};
return {
content: [
{
type: "text",
text: JSON.stringify(repoInfo, null, 2),
},
],
};
}
case "github_get_file": {
const { owner, repo, path, ref } = args as any;
const response = await octokit.repos.getContent({
owner,
repo,
path,
...(ref && { ref }),
});
if (Array.isArray(response.data)) {
throw new Error("Path is a directory, not a file");
}
if (response.data.type !== "file") {
throw new Error(`Path is a ${response.data.type}, not a file`);
}
// Decode base64 content
const content = Buffer.from(response.data.content, "base64").toString("utf-8");
return {
content: [
{
type: "text",
text: content,
},
],
};
}
case "github_list_issues": {
const { owner, repo, state = "open", labels, per_page = 30 } = args as any;
const response = await octokit.issues.listForRepo({
owner,
repo,
state,
labels,
per_page,
});
const simplified = response.data.map((issue) => ({
number: issue.number,
title: issue.title,
state: issue.state,
html_url: issue.html_url,
user: issue.user?.login,
labels: issue.labels.map((l) => (typeof l === "string" ? l : l.name)),
created_at: issue.created_at,
updated_at: issue.updated_at,
body: issue.body?.substring(0, 500), // Truncate body to 500 chars
}));
return {
content: [
{
type: "text",
text: JSON.stringify(simplified, null, 2),
},
],
};
}
case "github_search_code": {
const { query, per_page = 30 } = args as any;
const response = await octokit.search.code({
q: query,
per_page,
});
const simplified = response.data.items.map((item) => ({
name: item.name,
path: item.path,
html_url: item.html_url,
repository: item.repository.full_name,
}));
return {
content: [
{
type: "text",
text: JSON.stringify(
{
total_count: response.data.total_count,
items: simplified,
},
null,
2
),
},
],
};
}
case "github_create_issue": {
const { owner, repo, title, body, labels, assignees, milestone } = args as any;
const response = await octokit.issues.create({
owner,
repo,
title,
body,
labels,
assignees,
milestone,
});
const issueInfo = {
number: response.data.number,
title: response.data.title,
state: response.data.state,
html_url: response.data.html_url,
user: response.data.user?.login,
labels: response.data.labels.map((l) => (typeof l === "string" ? l : l.name)),
assignees: response.data.assignees?.map((a) => a.login),
created_at: response.data.created_at,
body: response.data.body,
};
return {
content: [
{
type: "text",
text: JSON.stringify(issueInfo, null, 2),
},
],
};
}
case "github_update_issue": {
const { owner, repo, issue_number, title, body, state, labels, assignees } = args as any;
const response = await octokit.issues.update({
owner,
repo,
issue_number,
title,
body,
state,
labels,
assignees,
});
const issueInfo = {
number: response.data.number,
title: response.data.title,
state: response.data.state,
html_url: response.data.html_url,
user: response.data.user?.login,
labels: response.data.labels.map((l) => (typeof l === "string" ? l : l.name)),
assignees: response.data.assignees?.map((a) => a.login),
updated_at: response.data.updated_at,
body: response.data.body,
};
return {
content: [
{
type: "text",
text: JSON.stringify(issueInfo, null, 2),
},
],
};
}
case "github_create_comment": {
const { owner, repo, issue_number, body } = args as any;
const response = await octokit.issues.createComment({
owner,
repo,
issue_number,
body,
});
const commentInfo = {
id: response.data.id,
html_url: response.data.html_url,
user: response.data.user?.login,
created_at: response.data.created_at,
body: response.data.body,
};
return {
content: [
{
type: "text",
text: JSON.stringify(commentInfo, null, 2),
},
],
};
}
case "github_create_repo": {
const { name, description, private: isPrivate, auto_init } = args as any;
const response = await octokit.repos.createForAuthenticatedUser({
name,
description,
private: isPrivate,
auto_init,
});
const repoInfo = {
name: response.data.name,
full_name: response.data.full_name,
description: response.data.description,
private: response.data.private,
html_url: response.data.html_url,
clone_url: response.data.clone_url,
created_at: response.data.created_at,
};
return {
content: [
{
type: "text",
text: JSON.stringify(repoInfo, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
// Handle GitHub API errors
if (error.status) {
return {
content: [
{
type: "text",
text: `GitHub API Error ${error.status}: ${error.message}`,
},
],
isError: true,
};
}
throw error;
}
});
}