v1.0.1
This commit is contained in:
777
src/mcp/gitea-mcp-server.ts
Normal file
777
src/mcp/gitea-mcp-server.ts
Normal file
@@ -0,0 +1,777 @@
|
||||
#!/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<any> {
|
||||
const url = `${GITEA_API_URL}${endpoint}`;
|
||||
console.log(`[GITEA-MCP] Making ${method} request to: ${url}`);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
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(
|
||||
`/api/v1/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 = `/api/v1/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(
|
||||
`/api/v1/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(
|
||||
`/api/v1/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(
|
||||
`/api/v1/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(
|
||||
`/api/v1/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 = `/api/v1/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(
|
||||
`/api/v1/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(
|
||||
`/api/v1/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(`/api/v1/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 = `/api/v1/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(
|
||||
`/api/v1/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(
|
||||
`/api/v1/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);
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
||||
import { readFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import fetch from "node-fetch";
|
||||
import { GITHUB_API_URL } from "../github/api/config";
|
||||
import { GITEA_API_URL } from "../github/api/config";
|
||||
|
||||
type GitHubRef = {
|
||||
object: {
|
||||
@@ -80,146 +80,16 @@ server.tool(
|
||||
return filePath;
|
||||
});
|
||||
|
||||
// 1. Get the branch reference
|
||||
const refUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
|
||||
const refResponse = await fetch(refUrl, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
});
|
||||
|
||||
if (!refResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to get branch reference: ${refResponse.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
const refData = (await refResponse.json()) as GitHubRef;
|
||||
const baseSha = refData.object.sha;
|
||||
|
||||
// 2. Get the base commit
|
||||
const commitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${baseSha}`;
|
||||
const commitResponse = await fetch(commitUrl, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
});
|
||||
|
||||
if (!commitResponse.ok) {
|
||||
throw new Error(`Failed to get base commit: ${commitResponse.status}`);
|
||||
}
|
||||
|
||||
const commitData = (await commitResponse.json()) as GitHubCommit;
|
||||
const baseTreeSha = commitData.tree.sha;
|
||||
|
||||
// 3. Create tree entries for all files
|
||||
const treeEntries = await Promise.all(
|
||||
processedFiles.map(async (filePath) => {
|
||||
const fullPath = filePath.startsWith("/")
|
||||
? filePath
|
||||
: join(REPO_DIR, filePath);
|
||||
|
||||
const content = await readFile(fullPath, "utf-8");
|
||||
return {
|
||||
path: filePath,
|
||||
mode: "100644",
|
||||
type: "blob",
|
||||
content: content,
|
||||
};
|
||||
}),
|
||||
// NOTE: Gitea does not support GitHub's low-level git API operations
|
||||
// (creating trees, commits, etc.). We need to use the contents API instead.
|
||||
// For now, throw an error indicating this functionality is not available.
|
||||
throw new Error(
|
||||
"Multi-file commits are not supported with Gitea. " +
|
||||
"Gitea does not provide the low-level git API operations (trees, commits) " +
|
||||
"that are required for atomic multi-file commits. " +
|
||||
"Please commit files individually using the contents API.",
|
||||
);
|
||||
|
||||
// 4. Create a new tree
|
||||
const treeUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/trees`;
|
||||
const treeResponse = await fetch(treeUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
base_tree: baseTreeSha,
|
||||
tree: treeEntries,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!treeResponse.ok) {
|
||||
const errorText = await treeResponse.text();
|
||||
throw new Error(
|
||||
`Failed to create tree: ${treeResponse.status} - ${errorText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const treeData = (await treeResponse.json()) as GitHubTree;
|
||||
|
||||
// 5. Create a new commit
|
||||
const newCommitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits`;
|
||||
const newCommitResponse = await fetch(newCommitUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
tree: treeData.sha,
|
||||
parents: [baseSha],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!newCommitResponse.ok) {
|
||||
const errorText = await newCommitResponse.text();
|
||||
throw new Error(
|
||||
`Failed to create commit: ${newCommitResponse.status} - ${errorText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const newCommitData = (await newCommitResponse.json()) as GitHubNewCommit;
|
||||
|
||||
// 6. Update the reference to point to the new commit
|
||||
const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
|
||||
const updateRefResponse = await fetch(updateRefUrl, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sha: newCommitData.sha,
|
||||
force: false,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!updateRefResponse.ok) {
|
||||
const errorText = await updateRefResponse.text();
|
||||
throw new Error(
|
||||
`Failed to update reference: ${updateRefResponse.status} - ${errorText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const simplifiedResult = {
|
||||
commit: {
|
||||
sha: newCommitData.sha,
|
||||
message: newCommitData.message,
|
||||
author: newCommitData.author.name,
|
||||
date: newCommitData.author.date,
|
||||
},
|
||||
files: processedFiles.map((path) => ({ path })),
|
||||
tree: {
|
||||
sha: treeData.sha,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@@ -283,136 +153,15 @@ server.tool(
|
||||
return filePath;
|
||||
});
|
||||
|
||||
// 1. Get the branch reference
|
||||
const refUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
|
||||
const refResponse = await fetch(refUrl, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
});
|
||||
|
||||
if (!refResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to get branch reference: ${refResponse.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
const refData = (await refResponse.json()) as GitHubRef;
|
||||
const baseSha = refData.object.sha;
|
||||
|
||||
// 2. Get the base commit
|
||||
const commitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${baseSha}`;
|
||||
const commitResponse = await fetch(commitUrl, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
});
|
||||
|
||||
if (!commitResponse.ok) {
|
||||
throw new Error(`Failed to get base commit: ${commitResponse.status}`);
|
||||
}
|
||||
|
||||
const commitData = (await commitResponse.json()) as GitHubCommit;
|
||||
const baseTreeSha = commitData.tree.sha;
|
||||
|
||||
// 3. Create tree entries for file deletions (setting SHA to null)
|
||||
const treeEntries = processedPaths.map((path) => ({
|
||||
path: path,
|
||||
mode: "100644",
|
||||
type: "blob" as const,
|
||||
sha: null,
|
||||
}));
|
||||
|
||||
// 4. Create a new tree with deletions
|
||||
const treeUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/trees`;
|
||||
const treeResponse = await fetch(treeUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
base_tree: baseTreeSha,
|
||||
tree: treeEntries,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!treeResponse.ok) {
|
||||
const errorText = await treeResponse.text();
|
||||
throw new Error(
|
||||
`Failed to create tree: ${treeResponse.status} - ${errorText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const treeData = (await treeResponse.json()) as GitHubTree;
|
||||
|
||||
// 5. Create a new commit
|
||||
const newCommitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits`;
|
||||
const newCommitResponse = await fetch(newCommitUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
tree: treeData.sha,
|
||||
parents: [baseSha],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!newCommitResponse.ok) {
|
||||
const errorText = await newCommitResponse.text();
|
||||
throw new Error(
|
||||
`Failed to create commit: ${newCommitResponse.status} - ${errorText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const newCommitData = (await newCommitResponse.json()) as GitHubNewCommit;
|
||||
|
||||
// 6. Update the reference to point to the new commit
|
||||
const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
|
||||
const updateRefResponse = await fetch(updateRefUrl, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sha: newCommitData.sha,
|
||||
force: false,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!updateRefResponse.ok) {
|
||||
const errorText = await updateRefResponse.text();
|
||||
throw new Error(
|
||||
`Failed to update reference: ${updateRefResponse.status} - ${errorText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const simplifiedResult = {
|
||||
commit: {
|
||||
sha: newCommitData.sha,
|
||||
message: newCommitData.message,
|
||||
author: newCommitData.author.name,
|
||||
date: newCommitData.author.date,
|
||||
},
|
||||
deletedFiles: processedPaths.map((path) => ({ path })),
|
||||
tree: {
|
||||
sha: treeData.sha,
|
||||
},
|
||||
};
|
||||
// NOTE: Gitea does not support GitHub's low-level git API operations
|
||||
// (creating trees, commits, etc.). We need to use the contents API instead.
|
||||
// For now, throw an error indicating this functionality is not available.
|
||||
throw new Error(
|
||||
"Multi-file deletions are not supported with Gitea. " +
|
||||
"Gitea does not provide the low-level git API operations (trees, commits) " +
|
||||
"that are required for atomic multi-file operations. " +
|
||||
"Please delete files individually using the contents API.",
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
|
||||
@@ -6,28 +6,28 @@ export async function prepareMcpConfig(
|
||||
repo: string,
|
||||
branch: string,
|
||||
): Promise<string> {
|
||||
console.log("[MCP-INSTALL] Preparing MCP configuration...");
|
||||
console.log(`[MCP-INSTALL] Owner: ${owner}`);
|
||||
console.log(`[MCP-INSTALL] Repo: ${repo}`);
|
||||
console.log(`[MCP-INSTALL] Branch: ${branch}`);
|
||||
console.log(
|
||||
`[MCP-INSTALL] GitHub token: ${githubToken ? "***" : "undefined"}`,
|
||||
);
|
||||
console.log(
|
||||
`[MCP-INSTALL] GITHUB_ACTION_PATH: ${process.env.GITHUB_ACTION_PATH}`,
|
||||
);
|
||||
console.log(
|
||||
`[MCP-INSTALL] GITHUB_WORKSPACE: ${process.env.GITHUB_WORKSPACE}`,
|
||||
);
|
||||
|
||||
try {
|
||||
const mcpConfig = {
|
||||
mcpServers: {
|
||||
github: {
|
||||
command: "docker",
|
||||
args: [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/anthropics/github-mcp-server:sha-7382253",
|
||||
],
|
||||
env: {
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
|
||||
},
|
||||
},
|
||||
github_file_ops: {
|
||||
command: "bun",
|
||||
args: [
|
||||
"run",
|
||||
`${process.env.GITHUB_ACTION_PATH}/src/mcp/github-file-ops-server.ts`,
|
||||
`${process.env.GITHUB_ACTION_PATH}/src/mcp/gitea-mcp-server.ts`,
|
||||
],
|
||||
env: {
|
||||
GITHUB_TOKEN: githubToken,
|
||||
@@ -35,13 +35,37 @@ export async function prepareMcpConfig(
|
||||
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: {
|
||||
command: "bun",
|
||||
args: [
|
||||
"run",
|
||||
`${process.env.GITHUB_ACTION_PATH}/src/mcp/local-git-ops-server.ts`,
|
||||
],
|
||||
env: {
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return JSON.stringify(mcpConfig, null, 2);
|
||||
const configString = JSON.stringify(mcpConfig, null, 2);
|
||||
console.log("[MCP-INSTALL] Generated MCP configuration:");
|
||||
console.log(configString);
|
||||
console.log("[MCP-INSTALL] MCP config generation completed successfully");
|
||||
|
||||
return configString;
|
||||
} catch (error) {
|
||||
console.error("[MCP-INSTALL] MCP config generation failed:", error);
|
||||
core.setFailed(`Install MCP server failed with error: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
410
src/mcp/local-git-ops-server.ts
Normal file
410
src/mcp/local-git-ops-server.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env node
|
||||
// Local Git Operations MCP Server
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
// Get repository information 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 REPO_DIR = process.env.REPO_DIR || process.cwd();
|
||||
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
|
||||
const GITEA_API_URL = process.env.GITEA_API_URL || "https://api.github.com";
|
||||
|
||||
console.log(`[LOCAL-GIT-MCP] Starting Local Git Operations MCP Server`);
|
||||
console.log(`[LOCAL-GIT-MCP] REPO_OWNER: ${REPO_OWNER}`);
|
||||
console.log(`[LOCAL-GIT-MCP] REPO_NAME: ${REPO_NAME}`);
|
||||
console.log(`[LOCAL-GIT-MCP] BRANCH_NAME: ${BRANCH_NAME}`);
|
||||
console.log(`[LOCAL-GIT-MCP] REPO_DIR: ${REPO_DIR}`);
|
||||
console.log(`[LOCAL-GIT-MCP] GITEA_API_URL: ${GITEA_API_URL}`);
|
||||
console.log(
|
||||
`[LOCAL-GIT-MCP] GITHUB_TOKEN: ${GITHUB_TOKEN ? "***" : "undefined"}`,
|
||||
);
|
||||
|
||||
if (!REPO_OWNER || !REPO_NAME || !BRANCH_NAME) {
|
||||
console.error(
|
||||
"[LOCAL-GIT-MCP] Error: REPO_OWNER, REPO_NAME, and BRANCH_NAME environment variables are required",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const server = new McpServer({
|
||||
name: "Local Git Operations Server",
|
||||
version: "0.0.1",
|
||||
});
|
||||
|
||||
// Helper function to run git commands
|
||||
function runGitCommand(command: string): string {
|
||||
try {
|
||||
console.log(`[LOCAL-GIT-MCP] Running git command: ${command}`);
|
||||
console.log(`[LOCAL-GIT-MCP] Working directory: ${REPO_DIR}`);
|
||||
const result = execSync(command, {
|
||||
cwd: REPO_DIR,
|
||||
encoding: "utf8",
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
console.log(`[LOCAL-GIT-MCP] Git command result: ${result.trim()}`);
|
||||
return result.trim();
|
||||
} catch (error: any) {
|
||||
console.error(`[LOCAL-GIT-MCP] Git command failed: ${command}`);
|
||||
console.error(`[LOCAL-GIT-MCP] Error: ${error.message}`);
|
||||
if (error.stdout) console.error(`[LOCAL-GIT-MCP] Stdout: ${error.stdout}`);
|
||||
if (error.stderr) console.error(`[LOCAL-GIT-MCP] Stderr: ${error.stderr}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
"Create a new branch from a base branch using local git operations",
|
||||
{
|
||||
branch_name: z.string().describe("Name of the branch to create"),
|
||||
base_branch: z
|
||||
.string()
|
||||
.describe("Base branch to create from (e.g., 'main')"),
|
||||
},
|
||||
async ({ branch_name, base_branch }) => {
|
||||
try {
|
||||
// Ensure we're on the base branch and it's up to date
|
||||
runGitCommand(`git checkout ${base_branch}`);
|
||||
runGitCommand(`git pull origin ${base_branch}`);
|
||||
|
||||
// Create and checkout the new branch
|
||||
runGitCommand(`git checkout -b ${branch_name}`);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Successfully created and checked out branch: ${branch_name}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error creating branch: ${errorMessage}`,
|
||||
},
|
||||
],
|
||||
error: errorMessage,
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Commit files tool
|
||||
server.tool(
|
||||
"commit_files",
|
||||
"Commit one or more files to the current branch using local git operations",
|
||||
{
|
||||
files: z
|
||||
.array(z.string())
|
||||
.describe(
|
||||
'Array of file paths relative to repository root (e.g. ["src/main.js", "README.md"]). All files must exist locally.',
|
||||
),
|
||||
message: z.string().describe("Commit message"),
|
||||
},
|
||||
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) {
|
||||
const filePath = file.startsWith("/") ? file.slice(1) : file;
|
||||
console.log(`[LOCAL-GIT-MCP] Adding file: ${filePath}`);
|
||||
runGitCommand(`git add "${filePath}"`);
|
||||
}
|
||||
|
||||
// Commit the changes
|
||||
console.log(`[LOCAL-GIT-MCP] Committing with message: ${message}`);
|
||||
runGitCommand(`git commit -m "${message}"`);
|
||||
|
||||
console.log(
|
||||
`[LOCAL-GIT-MCP] Successfully committed ${files.length} files`,
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Successfully committed ${files.length} file(s): ${files.join(", ")}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
console.error(`[LOCAL-GIT-MCP] Error committing files: ${errorMessage}`);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error committing files: ${errorMessage}`,
|
||||
},
|
||||
],
|
||||
error: errorMessage,
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Push branch tool
|
||||
server.tool(
|
||||
"push_branch",
|
||||
"Push the current branch to remote origin",
|
||||
{
|
||||
force: z.boolean().optional().describe("Force push (use with caution)"),
|
||||
},
|
||||
async ({ force = false }) => {
|
||||
try {
|
||||
// Get current branch name
|
||||
const currentBranch = runGitCommand("git rev-parse --abbrev-ref HEAD");
|
||||
|
||||
// Push the branch
|
||||
const pushCommand = force
|
||||
? `git push -f origin ${currentBranch}`
|
||||
: `git push origin ${currentBranch}`;
|
||||
|
||||
runGitCommand(pushCommand);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Successfully pushed branch: ${currentBranch}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error pushing branch: ${errorMessage}`,
|
||||
},
|
||||
],
|
||||
error: errorMessage,
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Create pull request tool (uses Gitea API)
|
||||
server.tool(
|
||||
"create_pull_request",
|
||||
"Create a pull request using Gitea API",
|
||||
{
|
||||
title: z.string().describe("Pull request title"),
|
||||
body: z.string().describe("Pull request body/description"),
|
||||
base_branch: z.string().describe("Base branch (e.g., 'main')"),
|
||||
head_branch: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Head branch (defaults to current branch)"),
|
||||
},
|
||||
async ({ title, body, base_branch, head_branch }) => {
|
||||
try {
|
||||
if (!GITHUB_TOKEN) {
|
||||
throw new Error(
|
||||
"GITHUB_TOKEN environment variable is required for PR creation",
|
||||
);
|
||||
}
|
||||
|
||||
// Get current branch if head_branch not specified
|
||||
const currentBranch =
|
||||
head_branch || runGitCommand("git rev-parse --abbrev-ref HEAD");
|
||||
|
||||
// Create PR using Gitea API
|
||||
const response = await fetch(
|
||||
`${GITEA_API_URL}/repos/${REPO_OWNER}/${REPO_NAME}/pulls`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `token ${GITHUB_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
body,
|
||||
base: base_branch,
|
||||
head: currentBranch,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to create PR: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
const prData = await response.json();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Successfully created pull request #${prData.number}: ${prData.html_url}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error creating pull request: ${errorMessage}`,
|
||||
},
|
||||
],
|
||||
error: errorMessage,
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Delete files tool
|
||||
server.tool(
|
||||
"delete_files",
|
||||
"Delete one or more files and commit the deletion using local git operations",
|
||||
{
|
||||
files: z
|
||||
.array(z.string())
|
||||
.describe(
|
||||
'Array of file paths relative to repository root (e.g. ["src/old-file.js", "docs/deprecated.md"])',
|
||||
),
|
||||
message: z.string().describe("Commit message for the deletion"),
|
||||
},
|
||||
async ({ files, message }) => {
|
||||
try {
|
||||
// Remove the specified files
|
||||
for (const file of files) {
|
||||
const filePath = file.startsWith("/") ? file.slice(1) : file;
|
||||
runGitCommand(`git rm "${filePath}"`);
|
||||
}
|
||||
|
||||
// Commit the deletions
|
||||
runGitCommand(`git commit -m "${message}"`);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Successfully deleted and committed ${files.length} file(s): ${files.join(", ")}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error deleting files: ${errorMessage}`,
|
||||
},
|
||||
],
|
||||
error: errorMessage,
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Get git status tool
|
||||
server.tool("git_status", "Get the current git status", {}, async () => {
|
||||
console.log(`[LOCAL-GIT-MCP] git_status called`);
|
||||
try {
|
||||
const status = runGitCommand("git status --porcelain");
|
||||
const currentBranch = runGitCommand("git rev-parse --abbrev-ref HEAD");
|
||||
|
||||
console.log(`[LOCAL-GIT-MCP] Current branch: ${currentBranch}`);
|
||||
console.log(
|
||||
`[LOCAL-GIT-MCP] Git status: ${status || "Working tree clean"}`,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Current branch: ${currentBranch}\nStatus:\n${status || "Working tree clean"}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[LOCAL-GIT-MCP] Error getting git status: ${errorMessage}`);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Error getting git status: ${errorMessage}`,
|
||||
},
|
||||
],
|
||||
error: errorMessage,
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function runServer() {
|
||||
console.log(`[LOCAL-GIT-MCP] Starting MCP server transport...`);
|
||||
const transport = new StdioServerTransport();
|
||||
console.log(`[LOCAL-GIT-MCP] Connecting to transport...`);
|
||||
await server.connect(transport);
|
||||
console.log(`[LOCAL-GIT-MCP] MCP server connected and ready!`);
|
||||
process.on("exit", () => {
|
||||
console.log(`[LOCAL-GIT-MCP] Server shutting down...`);
|
||||
server.close();
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[LOCAL-GIT-MCP] Calling runServer()...`);
|
||||
runServer().catch((error) => {
|
||||
console.error(`[LOCAL-GIT-MCP] Server startup failed:`, error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user