From 87a39e8fbc451438002071d33a7584286023f9a7 Mon Sep 17 00:00:00 2001 From: Mark Wylde Date: Sat, 31 May 2025 00:35:22 +0100 Subject: [PATCH] Attempt to make this work --- src/mcp/gitea-mcp-server.ts | 673 ++++++++++++++++++++++++++++++++ src/mcp/install-mcp-server.ts | 15 +- src/mcp/local-git-ops-server.ts | 24 ++ 3 files changed, 705 insertions(+), 7 deletions(-) create mode 100644 src/mcp/gitea-mcp-server.ts diff --git a/src/mcp/gitea-mcp-server.ts b/src/mcp/gitea-mcp-server.ts new file mode 100644 index 0000000..7cd2d63 --- /dev/null +++ b/src/mcp/gitea-mcp-server.ts @@ -0,0 +1,673 @@ +#!/usr/bin/env node +// Gitea API Operations MCP Server +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import fetch from "node-fetch"; + +// Get configuration from environment variables +const REPO_OWNER = process.env.REPO_OWNER; +const REPO_NAME = process.env.REPO_NAME; +const BRANCH_NAME = process.env.BRANCH_NAME; +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const GITEA_API_URL = process.env.GITEA_API_URL || "https://api.github.com"; + +console.log(`[GITEA-MCP] Starting Gitea API Operations MCP Server`); +console.log(`[GITEA-MCP] REPO_OWNER: ${REPO_OWNER}`); +console.log(`[GITEA-MCP] REPO_NAME: ${REPO_NAME}`); +console.log(`[GITEA-MCP] BRANCH_NAME: ${BRANCH_NAME}`); +console.log(`[GITEA-MCP] GITEA_API_URL: ${GITEA_API_URL}`); +console.log(`[GITEA-MCP] GITHUB_TOKEN: ${GITHUB_TOKEN ? '***' : 'undefined'}`); + +if (!REPO_OWNER || !REPO_NAME || !GITHUB_TOKEN) { + console.error( + "[GITEA-MCP] Error: REPO_OWNER, REPO_NAME, and GITHUB_TOKEN environment variables are required", + ); + process.exit(1); +} + +const server = new McpServer({ + name: "Gitea API Operations Server", + version: "0.0.1", +}); + +// Helper function to make authenticated requests to Gitea API +async function giteaRequest( + endpoint: string, + method: string = "GET", + body?: any, +): Promise { + const url = `${GITEA_API_URL}${endpoint}`; + console.log(`[GITEA-MCP] Making ${method} request to: ${url}`); + + const headers: Record = { + "Authorization": `token ${GITHUB_TOKEN}`, + "Accept": "application/json", + }; + + if (body) { + headers["Content-Type"] = "application/json"; + } + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + + const responseText = await response.text(); + console.log(`[GITEA-MCP] Response status: ${response.status}`); + console.log(`[GITEA-MCP] Response: ${responseText.substring(0, 500)}...`); + + if (!response.ok) { + throw new Error(`Gitea API request failed: ${response.status} ${responseText}`); + } + + return responseText ? JSON.parse(responseText) : null; +} + +// Get issue details +server.tool( + "get_issue", + "Get details of a specific issue", + { + issue_number: z.number().describe("The issue number to fetch"), + }, + async ({ issue_number }) => { + try { + const issue = await giteaRequest(`/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issue_number}`); + + return { + content: [ + { + type: "text", + text: JSON.stringify(issue, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[GITEA-MCP] Error getting issue: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error getting issue: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +// Get issue comments +server.tool( + "get_issue_comments", + "Get all comments for a specific issue", + { + issue_number: z.number().describe("The issue number to fetch comments for"), + since: z.string().optional().describe("Only show comments updated after this time (ISO 8601 format)"), + before: z.string().optional().describe("Only show comments updated before this time (ISO 8601 format)"), + }, + async ({ issue_number, since, before }) => { + try { + let endpoint = `/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issue_number}/comments`; + const params = new URLSearchParams(); + + if (since) params.append("since", since); + if (before) params.append("before", before); + + if (params.toString()) { + endpoint += `?${params.toString()}`; + } + + const comments = await giteaRequest(endpoint); + + return { + content: [ + { + type: "text", + text: JSON.stringify(comments, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[GITEA-MCP] Error getting issue comments: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error getting issue comments: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +// Add a comment to an issue +server.tool( + "add_issue_comment", + "Add a new comment to an issue", + { + issue_number: z.number().describe("The issue number to comment on"), + body: z.string().describe("The comment body content"), + }, + async ({ issue_number, body }) => { + try { + const comment = await giteaRequest( + `/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issue_number}/comments`, + "POST", + { body }, + ); + + return { + content: [ + { + type: "text", + text: JSON.stringify(comment, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[GITEA-MCP] Error adding issue comment: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error adding issue comment: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +// Update (edit) an issue comment +server.tool( + "update_issue_comment", + "Update an existing issue comment", + { + comment_id: z.number().describe("The comment ID to update"), + body: z.string().describe("The new comment body content"), + }, + async ({ comment_id, body }) => { + try { + const comment = await giteaRequest( + `/repos/${REPO_OWNER}/${REPO_NAME}/issues/comments/${comment_id}`, + "PATCH", + { body }, + ); + + return { + content: [ + { + type: "text", + text: JSON.stringify(comment, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[GITEA-MCP] Error updating issue comment: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error updating issue comment: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +// Delete an issue comment +server.tool( + "delete_issue_comment", + "Delete an issue comment", + { + comment_id: z.number().describe("The comment ID to delete"), + }, + async ({ comment_id }) => { + try { + await giteaRequest( + `/repos/${REPO_OWNER}/${REPO_NAME}/issues/comments/${comment_id}`, + "DELETE", + ); + + return { + content: [ + { + type: "text", + text: `Successfully deleted comment ${comment_id}`, + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[GITEA-MCP] Error deleting issue comment: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error deleting issue comment: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +// Get a specific comment +server.tool( + "get_comment", + "Get details of a specific comment", + { + comment_id: z.number().describe("The comment ID to fetch"), + }, + async ({ comment_id }) => { + try { + const comment = await giteaRequest(`/repos/${REPO_OWNER}/${REPO_NAME}/issues/comments/${comment_id}`); + + return { + content: [ + { + type: "text", + text: JSON.stringify(comment, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[GITEA-MCP] Error getting comment: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error getting comment: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +// List issues +server.tool( + "list_issues", + "List issues in the repository", + { + state: z.enum(["open", "closed", "all"]).optional().describe("Issue state filter"), + labels: z.string().optional().describe("Comma-separated list of label names"), + milestone: z.string().optional().describe("Milestone title to filter by"), + assignee: z.string().optional().describe("Username to filter issues assigned to"), + creator: z.string().optional().describe("Username to filter issues created by"), + mentioned: z.string().optional().describe("Username to filter issues that mention"), + page: z.number().optional().describe("Page number for pagination"), + limit: z.number().optional().describe("Number of items per page"), + }, + async ({ state, labels, milestone, assignee, creator, mentioned, page, limit }) => { + try { + let endpoint = `/repos/${REPO_OWNER}/${REPO_NAME}/issues`; + const params = new URLSearchParams(); + + if (state) params.append("state", state); + if (labels) params.append("labels", labels); + if (milestone) params.append("milestone", milestone); + if (assignee) params.append("assignee", assignee); + if (creator) params.append("creator", creator); + if (mentioned) params.append("mentioned", mentioned); + if (page) params.append("page", page.toString()); + if (limit) params.append("limit", limit.toString()); + + if (params.toString()) { + endpoint += `?${params.toString()}`; + } + + const issues = await giteaRequest(endpoint); + + return { + content: [ + { + type: "text", + text: JSON.stringify(issues, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[GITEA-MCP] Error listing issues: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error listing issues: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +// Create an issue +server.tool( + "create_issue", + "Create a new issue", + { + title: z.string().describe("Issue title"), + body: z.string().optional().describe("Issue body content"), + assignee: z.string().optional().describe("Username to assign the issue to"), + assignees: z.array(z.string()).optional().describe("Array of usernames to assign the issue to"), + milestone: z.number().optional().describe("Milestone ID to associate with the issue"), + labels: z.array(z.string()).optional().describe("Array of label names to apply to the issue"), + }, + async ({ title, body, assignee, assignees, milestone, labels }) => { + try { + const issueData: any = { title }; + + if (body) issueData.body = body; + if (assignee) issueData.assignee = assignee; + if (assignees) issueData.assignees = assignees; + if (milestone) issueData.milestone = milestone; + if (labels) issueData.labels = labels; + + const issue = await giteaRequest( + `/repos/${REPO_OWNER}/${REPO_NAME}/issues`, + "POST", + issueData, + ); + + return { + content: [ + { + type: "text", + text: JSON.stringify(issue, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[GITEA-MCP] Error creating issue: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error creating issue: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +// Update an issue +server.tool( + "update_issue", + "Update an existing issue", + { + issue_number: z.number().describe("The issue number to update"), + title: z.string().optional().describe("New issue title"), + body: z.string().optional().describe("New issue body content"), + assignee: z.string().optional().describe("Username to assign the issue to"), + assignees: z.array(z.string()).optional().describe("Array of usernames to assign the issue to"), + milestone: z.number().optional().describe("Milestone ID to associate with the issue"), + labels: z.array(z.string()).optional().describe("Array of label names to apply to the issue"), + state: z.enum(["open", "closed"]).optional().describe("Issue state"), + }, + async ({ issue_number, title, body, assignee, assignees, milestone, labels, state }) => { + try { + const updateData: any = {}; + + if (title) updateData.title = title; + if (body !== undefined) updateData.body = body; + if (assignee) updateData.assignee = assignee; + if (assignees) updateData.assignees = assignees; + if (milestone) updateData.milestone = milestone; + if (labels) updateData.labels = labels; + if (state) updateData.state = state; + + const issue = await giteaRequest( + `/repos/${REPO_OWNER}/${REPO_NAME}/issues/${issue_number}`, + "PATCH", + updateData, + ); + + return { + content: [ + { + type: "text", + text: JSON.stringify(issue, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[GITEA-MCP] Error updating issue: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error updating issue: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +// Get repository information +server.tool( + "get_repository", + "Get repository information", + {}, + async () => { + try { + const repo = await giteaRequest(`/repos/${REPO_OWNER}/${REPO_NAME}`); + + return { + content: [ + { + type: "text", + text: JSON.stringify(repo, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[GITEA-MCP] Error getting repository: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error getting repository: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +// Get pull requests +server.tool( + "list_pull_requests", + "List pull requests in the repository", + { + state: z.enum(["open", "closed", "all"]).optional().describe("Pull request state filter"), + head: z.string().optional().describe("Head branch name"), + base: z.string().optional().describe("Base branch name"), + page: z.number().optional().describe("Page number for pagination"), + limit: z.number().optional().describe("Number of items per page"), + }, + async ({ state, head, base, page, limit }) => { + try { + let endpoint = `/repos/${REPO_OWNER}/${REPO_NAME}/pulls`; + const params = new URLSearchParams(); + + if (state) params.append("state", state); + if (head) params.append("head", head); + if (base) params.append("base", base); + if (page) params.append("page", page.toString()); + if (limit) params.append("limit", limit.toString()); + + if (params.toString()) { + endpoint += `?${params.toString()}`; + } + + const pulls = await giteaRequest(endpoint); + + return { + content: [ + { + type: "text", + text: JSON.stringify(pulls, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[GITEA-MCP] Error listing pull requests: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error listing pull requests: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +// Get a specific pull request +server.tool( + "get_pull_request", + "Get details of a specific pull request", + { + pull_number: z.number().describe("The pull request number to fetch"), + }, + async ({ pull_number }) => { + try { + const pull = await giteaRequest(`/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${pull_number}`); + + return { + content: [ + { + type: "text", + text: JSON.stringify(pull, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[GITEA-MCP] Error getting pull request: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error getting pull request: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +// Create a pull request +server.tool( + "create_pull_request", + "Create a new pull request", + { + title: z.string().describe("Pull request title"), + body: z.string().optional().describe("Pull request body/description"), + head: z.string().describe("Head branch name"), + base: z.string().describe("Base branch name"), + assignee: z.string().optional().describe("Username to assign the pull request to"), + assignees: z.array(z.string()).optional().describe("Array of usernames to assign the pull request to"), + milestone: z.number().optional().describe("Milestone ID to associate with the pull request"), + labels: z.array(z.string()).optional().describe("Array of label names to apply to the pull request"), + }, + async ({ title, body, head, base, assignee, assignees, milestone, labels }) => { + try { + const pullData: any = { title, head, base }; + + if (body) pullData.body = body; + if (assignee) pullData.assignee = assignee; + if (assignees) pullData.assignees = assignees; + if (milestone) pullData.milestone = milestone; + if (labels) pullData.labels = labels; + + const pull = await giteaRequest( + `/repos/${REPO_OWNER}/${REPO_NAME}/pulls`, + "POST", + pullData, + ); + + return { + content: [ + { + type: "text", + text: JSON.stringify(pull, null, 2), + }, + ], + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`[GITEA-MCP] Error creating pull request: ${errorMessage}`); + return { + content: [ + { + type: "text", + text: `Error creating pull request: ${errorMessage}`, + }, + ], + error: errorMessage, + isError: true, + }; + } + }, +); + +async function runServer() { + console.log(`[GITEA-MCP] Starting MCP server transport...`); + const transport = new StdioServerTransport(); + console.log(`[GITEA-MCP] Connecting to transport...`); + await server.connect(transport); + console.log(`[GITEA-MCP] Gitea MCP server connected and ready!`); + process.on("exit", () => { + console.log(`[GITEA-MCP] Server shutting down...`); + server.close(); + }); +} + +console.log(`[GITEA-MCP] Calling runServer()...`); +runServer().catch((error) => { + console.error(`[GITEA-MCP] Server startup failed:`, error); + process.exit(1); +}); \ No newline at end of file diff --git a/src/mcp/install-mcp-server.ts b/src/mcp/install-mcp-server.ts index a0ef194..4373639 100644 --- a/src/mcp/install-mcp-server.ts +++ b/src/mcp/install-mcp-server.ts @@ -18,17 +18,18 @@ export async function prepareMcpConfig( const mcpConfig = { mcpServers: { github: { - command: "docker", + command: "bun", args: [ "run", - "-i", - "--rm", - "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/anthropics/github-mcp-server:sha-7382253", + `${process.env.GITHUB_ACTION_PATH}/src/mcp/gitea-mcp-server.ts`, ], env: { - GITHUB_PERSONAL_ACCESS_TOKEN: githubToken, + GITHUB_TOKEN: githubToken, + REPO_OWNER: owner, + REPO_NAME: repo, + BRANCH_NAME: branch, + REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(), + GITEA_API_URL: process.env.GITEA_API_URL || "https://api.github.com", }, }, local_git_ops: { diff --git a/src/mcp/local-git-ops-server.ts b/src/mcp/local-git-ops-server.ts index 72c6104..011c92f 100644 --- a/src/mcp/local-git-ops-server.ts +++ b/src/mcp/local-git-ops-server.ts @@ -56,6 +56,27 @@ function runGitCommand(command: string): string { } } +// Helper function to ensure git user is configured +function ensureGitUserConfigured(): void { + try { + // Check if user.email is already configured + runGitCommand("git config user.email"); + console.log(`[LOCAL-GIT-MCP] Git user.email already configured`); + } catch (error) { + console.log(`[LOCAL-GIT-MCP] Git user.email not configured, setting default`); + runGitCommand('git config user.email "claude@anthropic.com"'); + } + + try { + // Check if user.name is already configured + runGitCommand("git config user.name"); + console.log(`[LOCAL-GIT-MCP] Git user.name already configured`); + } catch (error) { + console.log(`[LOCAL-GIT-MCP] Git user.name not configured, setting default`); + runGitCommand('git config user.name "Claude"'); + } +} + // Create branch tool server.tool( "create_branch", @@ -115,6 +136,9 @@ server.tool( async ({ files, message }) => { console.log(`[LOCAL-GIT-MCP] commit_files called with files: ${JSON.stringify(files)}, message: ${message}`); try { + // Ensure git user is configured before committing + ensureGitUserConfigured(); + // Add the specified files console.log(`[LOCAL-GIT-MCP] Adding ${files.length} files to git...`); for (const file of files) {