MCP GitHub Issue Server

#!/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"; import { readFile } from "fs/promises"; import { fileURLToPath } from "url"; import { dirname, resolve } from "path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); interface IssueDetails { title: string; body: string; url: string; } class GitHubIssueServer { private server: Server; private octokit: Octokit; private async initialize() { const packageJsonPath = resolve(__dirname, "../package.json"); const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8")); this.server = new Server( { name: "github-issue-server", version: packageJson.version, }, { capabilities: { tools: {}, }, }, ); // Initialize Octokit without auth for public repos this.octokit = new Octokit(); this.setupToolHandlers(); this.server.onerror = (error) => console.error("[MCP Error]", error); process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } constructor() { // Properties will be initialized in initialize() this.server = {} as Server; this.octokit = {} as Octokit; } private parseGitHubUrl(url: string): { owner: string; repo: string; issue_number: number; } { const match = url.match(/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)/); if (!match) { throw new McpError( ErrorCode.InvalidParams, "Invalid GitHub issue URL format. Expected: https://github.com/owner/repo/issues/number", ); } return { owner: match[1], repo: match[2], issue_number: parseInt(match[3], 10), }; } private async getIssueDetails(url: string): Promise<IssueDetails> { const { owner, repo, issue_number } = this.parseGitHubUrl(url); try { const response = await this.octokit.issues.get({ owner, repo, issue_number, }); return { title: response.data.title, body: response.data.body || "", url: response.data.html_url, }; } catch (error) { if (error instanceof Error) { throw new McpError( ErrorCode.InternalError, `GitHub API error: ${error.message}`, ); } throw error; } } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "get_issue_task", description: "Fetch GitHub issue details to use as a task", inputSchema: { type: "object", properties: { url: { type: "string", description: "GitHub issue URL (https://github.com/owner/repo/issues/number)", }, }, required: ["url"], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== "get_issue_task") { throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`, ); } const { url } = request.params.arguments as { url: string }; if (!url) { throw new McpError( ErrorCode.InvalidParams, "URL parameter is required", ); } try { const issue = await this.getIssueDetails(url); return { content: [ { type: "text", text: JSON.stringify( { task: { title: issue.title, description: issue.body, source: issue.url, }, }, null, 2, ), }, ], }; } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Unexpected error: ${error}`, ); } }); } async run() { await this.initialize(); const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("GitHub Task MCP server running on stdio"); } } const server = new GitHubIssueServer(); server.run().catch(console.error);