#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
import { Octokit } from "@octokit/rest";
interface GitHubConfig {
token: string;
owner: string;
baseUrl?: string;
}
class GitHubMCP {
private config: GitHubConfig;
private server: Server;
private octokit: Octokit;
constructor() {
this.config = {
token: process.env.GITHUB_TOKEN || "",
owner: process.env.GITHUB_OWNER || "",
baseUrl: process.env.GITHUB_BASE_URL || "https://api.github.com",
};
if (!this.config.token) {
throw new Error("GITHUB_TOKEN environment variable is required");
}
if (!this.config.owner) {
throw new Error("GITHUB_OWNER environment variable is required");
}
this.octokit = new Octokit({
auth: this.config.token,
baseUrl: this.config.baseUrl,
});
this.server = new Server(
{
name: "github-mcp",
version: "0.2.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
// Existing repository tools
{
name: "list_repositories",
description: "List repositories for the authenticated user or organization",
inputSchema: {
type: "object",
properties: {
type: {
type: "string",
description: "Repository type: all, owner, member (default: owner)",
enum: ["all", "owner", "member"],
},
sort: {
type: "string",
description: "Sort by: created, updated, pushed, full_name (default: updated)",
enum: ["created", "updated", "pushed", "full_name"],
},
per_page: {
type: "number",
description: "Number of repositories per page (default: 30, max: 100)",
},
},
},
},
{
name: "get_repository",
description: "Get detailed information about a specific repository",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
owner: {
type: "string",
description: "Repository owner (optional, defaults to configured owner)",
},
},
required: ["repo"],
},
},
{
name: "get_file_content",
description: "Get content of a file from a repository",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
path: {
type: "string",
description: "File path",
},
ref: {
type: "string",
description: "Branch, tag, or commit SHA (optional, defaults to default branch)",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo", "path"],
},
},
{
name: "search_code",
description: "Search for code across repositories",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query",
},
repo: {
type: "string",
description: "Limit search to specific repository (optional)",
},
language: {
type: "string",
description: "Programming language filter (optional)",
},
},
required: ["query"],
},
},
{
name: "list_issues",
description: "List issues in a repository",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
state: {
type: "string",
description: "Issue state: open, closed, all (default: open)",
enum: ["open", "closed", "all"],
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo"],
},
},
{
name: "create_issue",
description: "Create a new issue",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
title: {
type: "string",
description: "Issue title",
},
body: {
type: "string",
description: "Issue description",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo", "title"],
},
},
{
name: "list_pull_requests",
description: "List pull requests in a repository",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
state: {
type: "string",
description: "PR state: open, closed, all (default: open)",
enum: ["open", "closed", "all"],
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo"],
},
},
{
name: "list_commits",
description: "List commits in a repository",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
sha: {
type: "string",
description: "Branch or commit SHA to start from",
},
per_page: {
type: "number",
description: "Results per page (default: 30, max: 100)",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo"],
},
},
{
name: "get_user_info",
description: "Get information about the authenticated user",
inputSchema: {
type: "object",
properties: {},
},
},
// NEW: Workflow tools
{
name: "list_workflows",
description: "List all workflows in a repository",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo"],
},
},
{
name: "get_workflow",
description: "Get details of a specific workflow",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
workflow_id: {
type: "string",
description: "Workflow ID or filename",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo", "workflow_id"],
},
},
{
name: "list_workflow_runs",
description: "List workflow runs for a repository or specific workflow",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
workflow_id: {
type: "string",
description: "Workflow ID (optional)",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
status: {
type: "string",
description: "Filter by status: queued, in_progress, completed",
enum: ["queued", "in_progress", "completed"],
},
branch: {
type: "string",
description: "Filter by branch",
},
per_page: {
type: "number",
description: "Results per page (default: 30)",
},
},
required: ["repo"],
},
},
{
name: "get_workflow_run",
description: "Get details of a specific workflow run",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
run_id: {
type: "number",
description: "Workflow run ID",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo", "run_id"],
},
},
{
name: "get_workflow_run_jobs",
description: "Get jobs for a workflow run",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
run_id: {
type: "number",
description: "Workflow run ID",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo", "run_id"],
},
},
{
name: "cancel_workflow_run",
description: "Cancel a workflow run",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
run_id: {
type: "number",
description: "Workflow run ID",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo", "run_id"],
},
},
{
name: "rerun_workflow",
description: "Re-run a workflow run",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
run_id: {
type: "number",
description: "Workflow run ID",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo", "run_id"],
},
},
{
name: "rerun_failed_jobs",
description: "Re-run failed jobs in a workflow run",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
run_id: {
type: "number",
description: "Workflow run ID",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo", "run_id"],
},
},
{
name: "trigger_workflow_dispatch",
description: "Trigger a workflow dispatch event",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
workflow_id: {
type: "string",
description: "Workflow ID or filename",
},
ref: {
type: "string",
description: "Git ref (default: main)",
},
inputs: {
type: "object",
description: "Workflow inputs",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo", "workflow_id"],
},
},
{
name: "list_workflow_artifacts",
description: "List artifacts for a workflow run or repository",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
run_id: {
type: "number",
description: "Workflow run ID (optional)",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo"],
},
},
{
name: "download_artifact",
description: "Get download URL for a workflow artifact",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
artifact_id: {
type: "number",
description: "Artifact ID",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo", "artifact_id"],
},
},
{
name: "get_workflow_usage",
description: "Get workflow usage statistics",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
},
workflow_id: {
type: "string",
description: "Workflow ID",
},
owner: {
type: "string",
description: "Repository owner (optional)",
},
},
required: ["repo", "workflow_id"],
},
},
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (!args) {
throw new McpError(ErrorCode.InvalidParams, "Missing arguments");
}
try {
switch (name) {
// Existing tools
case "list_repositories":
return await this.listRepositories(
args.type as string,
args.sort as string,
args.per_page as number
);
case "get_repository":
return await this.getRepository(
args.repo as string,
args.owner as string
);
case "get_file_content":
return await this.getFileContent(
args.repo as string,
args.path as string,
args.ref as string,
args.owner as string
);
case "search_code":
return await this.searchCode(
args.query as string,
args.repo as string,
args.language as string
);
case "list_issues":
return await this.listIssues(
args.repo as string,
args.state as string,
args.owner as string
);
case "create_issue":
return await this.createIssue(
args.repo as string,
args.title as string,
args.body as string,
args.owner as string
);
case "list_pull_requests":
return await this.listPullRequests(
args.repo as string,
args.state as string,
args.owner as string
);
case "list_commits":
return await this.listCommits(
args.repo as string,
args.sha as string,
args.per_page as number,
args.owner as string
);
case "get_user_info":
return await this.getUserInfo();
// NEW: Workflow tools
case "list_workflows":
return await this.listWorkflows(
args.repo as string,
args.owner as string
);
case "get_workflow":
return await this.getWorkflow(
args.repo as string,
args.workflow_id as string,
args.owner as string
);
case "list_workflow_runs":
return await this.listWorkflowRuns(
args.repo as string,
args.workflow_id as string,
args.owner as string,
args.status as string,
args.branch as string,
args.per_page as number
);
case "get_workflow_run":
return await this.getWorkflowRun(
args.repo as string,
args.run_id as number,
args.owner as string
);
case "get_workflow_run_jobs":
return await this.getWorkflowRunJobs(
args.repo as string,
args.run_id as number,
args.owner as string
);
case "cancel_workflow_run":
return await this.cancelWorkflowRun(
args.repo as string,
args.run_id as number,
args.owner as string
);
case "rerun_workflow":
return await this.rerunWorkflow(
args.repo as string,
args.run_id as number,
args.owner as string
);
case "rerun_failed_jobs":
return await this.rerunFailedJobs(
args.repo as string,
args.run_id as number,
args.owner as string
);
case "trigger_workflow_dispatch":
return await this.triggerWorkflowDispatch(
args.repo as string,
args.workflow_id as string,
args.ref as string,
args.inputs as Record<string, any>,
args.owner as string
);
case "list_workflow_artifacts":
return await this.listWorkflowArtifacts(
args.repo as string,
args.run_id as number,
args.owner as string
);
case "download_artifact":
return await this.downloadArtifact(
args.repo as string,
args.artifact_id as number,
args.owner as string
);
case "get_workflow_usage":
return await this.getWorkflowUsage(
args.repo as string,
args.workflow_id as string,
args.owner as string
);
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
} catch (error: any) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error.message}`);
}
});
}
private getOwner(owner?: string): string {
return owner || this.config.owner;
}
// Existing methods (unchanged)
private async listRepositories(type?: string, sort?: string, perPage?: number) {
try {
const { data } = await this.octokit.repos.listForAuthenticatedUser({
type: (type as any) || 'owner',
sort: (sort as any) || 'updated',
per_page: perPage || 30,
});
return {
content: [
{
type: "text",
text: `**Repositories (${data.length}):**\n\n` +
data.map(repo =>
`β’ **${repo.name}** ${repo.private ? 'π' : 'π'}\n` +
` ${repo.description || 'No description'}\n` +
` Language: ${repo.language || 'N/A'} | Stars: ${repo.stargazers_count} | Forks: ${repo.forks_count}\n` +
` Updated: ${new Date(repo.updated_at).toLocaleDateString()}\n` +
` URL: ${repo.html_url}`
).join('\n\n')
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to list repositories: ${error.message}`);
}
}
private async getRepository(repo: string, owner?: string) {
try {
const { data } = await this.octokit.repos.get({
owner: this.getOwner(owner),
repo,
});
return {
content: [
{
type: "text",
text: `**Repository: ${data.full_name}** ${data.private ? 'π' : 'π'}\n\n` +
`β’ **Description**: ${data.description || 'No description'}\n` +
`β’ **Language**: ${data.language || 'N/A'}\n` +
`β’ **Default Branch**: ${data.default_branch}\n` +
`β’ **Stars**: ${data.stargazers_count}\n` +
`β’ **Forks**: ${data.forks_count}\n` +
`β’ **Issues**: ${data.open_issues_count} open\n` +
`β’ **Size**: ${data.size} KB\n` +
`β’ **Created**: ${new Date(data.created_at).toLocaleDateString()}\n` +
`β’ **Updated**: ${new Date(data.updated_at).toLocaleDateString()}\n` +
`β’ **Clone URL**: ${data.clone_url}\n` +
`β’ **Web URL**: ${data.html_url}`
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to get repository: ${error.message}`);
}
}
private async getFileContent(repo: string, path: string, ref?: string, owner?: string) {
try {
const { data } = await this.octokit.repos.getContent({
owner: this.getOwner(owner),
repo,
path,
ref,
});
if (Array.isArray(data)) {
return {
content: [
{
type: "text",
text: `**Directory: ${path}**\n\n` +
data.map(item =>
`${item.type === 'dir' ? 'π' : 'π'} **${item.name}** (${item.size || 0} bytes)`
).join('\n')
}
]
};
}
if (data.type === 'file' && 'content' in data) {
const content = Buffer.from(data.content, 'base64').toString('utf8');
return {
content: [
{
type: "text",
text: `**File: ${path}** (${data.size} bytes)\n\n\`\`\`\n${content}\n\`\`\``
}
]
};
}
return {
content: [
{
type: "text",
text: `**${data.type}: ${path}**\n\nURL: ${data.html_url}`
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to get file content: ${error.message}`);
}
}
private async searchCode(query: string, repo?: string, language?: string) {
try {
let searchQuery = query;
if (repo) searchQuery += ` repo:${this.getOwner()}/${repo}`;
if (language) searchQuery += ` language:${language}`;
const { data } = await this.octokit.search.code({
q: searchQuery,
per_page: 30,
});
return {
content: [
{
type: "text",
text: `**Code Search Results (${data.total_count} total):**\n\n` +
data.items.map(item =>
`β’ **${item.name}** in ${item.repository.full_name}\n` +
` Path: ${item.path}\n` +
` URL: ${item.html_url}`
).join('\n\n')
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to search code: ${error.message}`);
}
}
private async listIssues(repo: string, state?: string, owner?: string) {
try {
const { data } = await this.octokit.issues.listForRepo({
owner: this.getOwner(owner),
repo,
state: (state as any) || 'open',
});
return {
content: [
{
type: "text",
text: `**Issues in ${repo} (${state || 'open'}):**\n\n` +
data.map(issue =>
`β’ **#${issue.number}**: ${issue.title}\n` +
` State: ${issue.state} | Created: ${new Date(issue.created_at).toLocaleDateString()}\n` +
` Author: ${issue.user?.login}\n` +
` URL: ${issue.html_url}`
).join('\n\n')
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to list issues: ${error.message}`);
}
}
private async createIssue(repo: string, title: string, body?: string, owner?: string) {
try {
const { data } = await this.octokit.issues.create({
owner: this.getOwner(owner),
repo,
title,
body,
});
return {
content: [
{
type: "text",
text: `**β
Issue Created Successfully!**\n\n` +
`β’ **Issue #${data.number}**: ${data.title}\n` +
`β’ **URL**: ${data.html_url}\n` +
`β’ **State**: ${data.state}\n` +
`β’ **Created**: ${new Date(data.created_at).toLocaleDateString()}`
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to create issue: ${error.message}`);
}
}
private async listPullRequests(repo: string, state?: string, owner?: string) {
try {
const { data } = await this.octokit.pulls.list({
owner: this.getOwner(owner),
repo,
state: (state as any) || 'open',
});
return {
content: [
{
type: "text",
text: `**Pull Requests in ${repo} (${state || 'open'}):**\n\n` +
data.map(pr =>
`β’ **#${pr.number}**: ${pr.title}\n` +
` ${pr.head.ref} β ${pr.base.ref}\n` +
` State: ${pr.state} | Author: ${pr.user?.login}\n` +
` Created: ${new Date(pr.created_at).toLocaleDateString()}\n` +
` URL: ${pr.html_url}`
).join('\n\n')
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to list pull requests: ${error.message}`);
}
}
private async listCommits(repo: string, sha?: string, perPage?: number, owner?: string) {
try {
const { data } = await this.octokit.repos.listCommits({
owner: this.getOwner(owner),
repo,
sha,
per_page: perPage || 30,
});
return {
content: [
{
type: "text",
text: `**Recent Commits in ${repo}:**\n\n` +
data.map(commit =>
`β’ **${commit.sha.substring(0, 7)}**: ${commit.commit.message.split('\n')[0]}\n` +
` Author: ${commit.commit.author?.name} | Date: ${new Date(commit.commit.author?.date || '').toLocaleDateString()}\n` +
` URL: ${commit.html_url}`
).join('\n\n')
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to list commits: ${error.message}`);
}
}
private async getUserInfo() {
try {
const { data } = await this.octokit.users.getAuthenticated();
return {
content: [
{
type: "text",
text: `**User: ${data.login}** ${data.type === 'Organization' ? 'π’' : 'π€'}\n\n` +
`β’ **Name**: ${data.name || 'Not provided'}\n` +
`β’ **Bio**: ${data.bio || 'No bio'}\n` +
`β’ **Company**: ${data.company || 'Not provided'}\n` +
`β’ **Location**: ${data.location || 'Not provided'}\n` +
`β’ **Email**: ${data.email || 'Not public'}\n` +
`β’ **Public Repos**: ${data.public_repos}\n` +
`β’ **Followers**: ${data.followers} | **Following**: ${data.following}\n` +
`β’ **Created**: ${new Date(data.created_at).toLocaleDateString()}\n` +
`β’ **Updated**: ${new Date(data.updated_at).toLocaleDateString()}\n` +
`β’ **Profile**: ${data.html_url}`
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to get user info: ${error.message}`);
}
}
// NEW: Workflow methods
private async listWorkflows(repo: string, owner?: string) {
try {
const { data } = await this.octokit.actions.listRepoWorkflows({
owner: this.getOwner(owner),
repo,
});
return {
content: [
{
type: "text",
text: `**π Workflows in ${repo} (${data.total_count}):**\n\n` +
data.workflows.map(workflow =>
`β’ **${workflow.name}** (ID: ${workflow.id})\n` +
` Path: ${workflow.path}\n` +
` State: ${workflow.state}\n` +
` Created: ${new Date(workflow.created_at).toLocaleDateString()}\n` +
` Updated: ${new Date(workflow.updated_at).toLocaleDateString()}\n` +
` URL: ${workflow.html_url}`
).join('\n\n')
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to list workflows: ${error.message}`);
}
}
private async getWorkflow(repo: string, workflowId: string, owner?: string) {
try {
const { data } = await this.octokit.actions.getWorkflow({
owner: this.getOwner(owner),
repo,
workflow_id: workflowId,
});
return {
content: [
{
type: "text",
text: `**π Workflow: ${data.name}**\n\n` +
`β’ **ID**: ${data.id}\n` +
`β’ **Path**: ${data.path}\n` +
`β’ **State**: ${data.state}\n` +
`β’ **Created**: ${new Date(data.created_at).toLocaleDateString()}\n` +
`β’ **Updated**: ${new Date(data.updated_at).toLocaleDateString()}\n` +
`β’ **URL**: ${data.html_url}\n` +
`β’ **Badge URL**: ${data.badge_url}`
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to get workflow: ${error.message}`);
}
}
private async listWorkflowRuns(repo: string, workflowId?: string, owner?: string, status?: string, branch?: string, perPage?: number) {
try {
let response;
const params: any = {
owner: this.getOwner(owner),
repo,
per_page: perPage || 30,
};
if (status) params.status = status;
if (branch) params.branch = branch;
if (workflowId) {
response = await this.octokit.actions.listWorkflowRuns({
...params,
workflow_id: workflowId,
});
} else {
response = await this.octokit.actions.listWorkflowRunsForRepo(params);
}
return {
content: [
{
type: "text",
text: `**π Workflow Runs in ${repo} (${response.data.total_count} total):**\n\n` +
response.data.workflow_runs.map(run => {
const statusEmoji = {
'completed': run.conclusion === 'success' ? 'β
' : run.conclusion === 'failure' ? 'β' : 'β οΈ',
'in_progress': 'π',
'queued': 'β³'
}[run.status] || 'β';
return `${statusEmoji} **Run #${run.run_number}** (ID: ${run.id})\n` +
` Workflow: ${run.name}\n` +
` Status: ${run.status} (${run.conclusion || 'pending'})\n` +
` Branch: ${run.head_branch}\n` +
` Commit: ${run.head_sha.substring(0, 8)}\n` +
` Actor: ${run.actor?.login}\n` +
` Created: ${new Date(run.created_at).toLocaleDateString()}\n` +
` URL: ${run.html_url}`;
}).join('\n\n')
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to list workflow runs: ${error.message}`);
}
}
private async getWorkflowRun(repo: string, runId: number, owner?: string) {
try {
const { data } = await this.octokit.actions.getWorkflowRun({
owner: this.getOwner(owner),
repo,
run_id: runId,
});
const statusEmoji = {
'completed': data.conclusion === 'success' ? 'β
' : data.conclusion === 'failure' ? 'β' : 'β οΈ',
'in_progress': 'π',
'queued': 'β³'
}[data.status] || 'β';
return {
content: [
{
type: "text",
text: `${statusEmoji} **Workflow Run #${data.run_number}**\n\n` +
`β’ **ID**: ${data.id}\n` +
`β’ **Workflow**: ${data.name}\n` +
`β’ **Status**: ${data.status}\n` +
`β’ **Conclusion**: ${data.conclusion || 'N/A'}\n` +
`β’ **Branch**: ${data.head_branch}\n` +
`β’ **Commit**: ${data.head_sha}\n` +
`β’ **Actor**: ${data.actor?.login}\n` +
`β’ **Event**: ${data.event}\n` +
`β’ **Created**: ${new Date(data.created_at).toLocaleDateString()}\n` +
`β’ **Updated**: ${new Date(data.updated_at).toLocaleDateString()}\n` +
`β’ **Run Started**: ${data.run_started_at ? new Date(data.run_started_at).toLocaleDateString() : 'N/A'}\n` +
`β’ **URL**: ${data.html_url}`
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to get workflow run: ${error.message}`);
}
}
private async getWorkflowRunJobs(repo: string, runId: number, owner?: string) {
try {
const { data } = await this.octokit.actions.listJobsForWorkflowRun({
owner: this.getOwner(owner),
repo,
run_id: runId,
});
return {
content: [
{
type: "text",
text: `**π οΈ Jobs for Workflow Run #${runId}:**\n\n` +
data.jobs.map(job => {
const statusEmoji = {
'completed': job.conclusion === 'success' ? 'β
' : job.conclusion === 'failure' ? 'β' : 'β οΈ',
'in_progress': 'π',
'queued': 'β³'
}[job.status] || 'β';
return `${statusEmoji} **${job.name}** (ID: ${job.id})\n` +
` Status: ${job.status}\n` +
` Conclusion: ${job.conclusion || 'N/A'}\n` +
` Started: ${job.started_at ? new Date(job.started_at).toLocaleString() : 'N/A'}\n` +
` Completed: ${job.completed_at ? new Date(job.completed_at).toLocaleString() : 'N/A'}\n` +
` Runner: ${job.runner_name || 'N/A'}\n` +
` URL: ${job.html_url}`;
}).join('\n\n')
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to get workflow run jobs: ${error.message}`);
}
}
private async cancelWorkflowRun(repo: string, runId: number, owner?: string) {
try {
await this.octokit.actions.cancelWorkflowRun({
owner: this.getOwner(owner),
repo,
run_id: runId,
});
return {
content: [
{
type: "text",
text: `**β
Workflow run #${runId} cancelled successfully!**\n\nThe workflow run has been cancelled and should stop processing.`
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to cancel workflow run: ${error.message}`);
}
}
private async rerunWorkflow(repo: string, runId: number, owner?: string) {
try {
await this.octokit.actions.reRunWorkflow({
owner: this.getOwner(owner),
repo,
run_id: runId,
});
return {
content: [
{
type: "text",
text: `**π Workflow run #${runId} re-run triggered successfully!**\n\nThe entire workflow run will be re-executed.`
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to re-run workflow: ${error.message}`);
}
}
private async rerunFailedJobs(repo: string, runId: number, owner?: string) {
try {
await this.octokit.actions.reRunWorkflowFailedJobs({
owner: this.getOwner(owner),
repo,
run_id: runId,
});
return {
content: [
{
type: "text",
text: `**π Failed jobs in workflow run #${runId} re-run triggered successfully!**\n\nOnly the failed jobs will be re-executed.`
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to re-run failed jobs: ${error.message}`);
}
}
private async triggerWorkflowDispatch(repo: string, workflowId: string, ref?: string, inputs?: Record<string, any>, owner?: string) {
try {
await this.octokit.actions.createWorkflowDispatch({
owner: this.getOwner(owner),
repo,
workflow_id: workflowId,
ref: ref || 'main',
inputs: inputs || {},
});
return {
content: [
{
type: "text",
text: `**π Workflow dispatch triggered successfully!**\n\n` +
`β’ **Workflow**: ${workflowId}\n` +
`β’ **Repository**: ${repo}\n` +
`β’ **Branch/Tag**: ${ref || 'main'}\n` +
`β’ **Inputs**: ${inputs ? JSON.stringify(inputs, null, 2) : 'None'}\n\n` +
`The workflow should start running shortly. Check the Actions tab to monitor progress.`
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to trigger workflow dispatch: ${error.message}`);
}
}
private async listWorkflowArtifacts(repo: string, runId?: number, owner?: string) {
try {
let response;
if (runId) {
response = await this.octokit.actions.listWorkflowRunArtifacts({
owner: this.getOwner(owner),
repo,
run_id: runId,
});
} else {
response = await this.octokit.actions.listArtifactsForRepo({
owner: this.getOwner(owner),
repo,
});
}
return {
content: [
{
type: "text",
text: `**π¦ Workflow Artifacts (${response.data.total_count} total):**\n\n` +
response.data.artifacts.map(artifact =>
`β’ **${artifact.name}** (ID: ${artifact.id})\n` +
` Size: ${Math.round(artifact.size_in_bytes / 1024)} KB\n` +
` Created: ${new Date(artifact.created_at).toLocaleDateString()}\n` +
` Expires: ${new Date(artifact.expires_at).toLocaleDateString()}\n` +
` Workflow Run: ${artifact.workflow_run?.id || 'N/A'}`
).join('\n\n') || 'No artifacts found.'
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to list workflow artifacts: ${error.message}`);
}
}
private async downloadArtifact(repo: string, artifactId: number, owner?: string) {
try {
const { data } = await this.octokit.actions.downloadArtifact({
owner: this.getOwner(owner),
repo,
artifact_id: artifactId,
archive_format: 'zip',
});
return {
content: [
{
type: "text",
text: `**π₯ Artifact Download Information**\n\n` +
`β’ **Artifact ID**: ${artifactId}\n` +
`β’ **Repository**: ${repo}\n` +
`β’ **Download URL**: Available via GitHub API\n` +
`β’ **Format**: ZIP archive\n\n` +
`Note: The artifact download URL is temporary and can be used to download the artifact programmatically.`
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to get artifact download info: ${error.message}`);
}
}
private async getWorkflowUsage(repo: string, workflowId: string, owner?: string) {
try {
const { data } = await this.octokit.actions.getWorkflowUsage({
owner: this.getOwner(owner),
repo,
workflow_id: workflowId,
});
return {
content: [
{
type: "text",
text: `**π Workflow Usage Statistics**\n\n` +
Object.entries(data.billable).map(([os, usage]: [string, any]) =>
`**${os.toUpperCase()}**:\n` +
` β’ Total time: ${Math.round(usage.total_ms / 1000)}s (${usage.total_ms}ms)\n` +
` β’ Jobs: ${usage.jobs}`
).join('\n\n')
}
]
};
} catch (error: any) {
throw new McpError(ErrorCode.InternalError, `Failed to get workflow usage: ${error.message}`);
}
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("GitHub MCP server with workflow capabilities running on stdio");
}
}
const server = new GitHubMCP();
server.run().catch(console.error);