index.tsā¢5.98 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { exec } from "child_process";
import { promisify } from "util";
import * as fs from 'fs/promises';
import * as path from 'path';
const execAsync = promisify(exec);
const CWD_MARKER = "CWD_MARKER_EXEC_FINISH";
const WORKSPACE_DIR = "/workspace"; // All operations will be contained here for security
export default function createServer({ config }) {
// Configure git on startup to use the GITHUB_PAT for all https://github.com operations.
// This allows users to run `git clone/push/pull` in the terminal without manual auth.
const configureGit = async () => {
const token = process.env.GITHUB_PAT;
if (token) {
try {
// This tells git: whenever you see a URL that starts with "https://github.com/",
// replace it with "https://<token>@github.com/" before making the request.
await execAsync(`git config --global url."https://${token}@github.com/".insteadOf "https://github.com/"`);
// Set a default user for commits made from the terminal
await execAsync(`git config --global user.name "PyForge User"`);
await execAsync(`git config --global user.email "user@pyforge.dev"`);
console.log("Git PAT and user configured globally for terminal use.");
} catch (e) {
console.error("Failed to configure git globally:", e);
}
} else {
console.warn("GITHUB_PAT not set. Git operations in the terminal requiring auth may fail.");
}
};
configureGit();
const server = new McpServer({
name: "PyForge Bash MCP Server",
version: "1.0.0",
});
// Ensure workspace directory exists on startup
fs.mkdir(WORKSPACE_DIR, { recursive: true }).catch(console.error);
server.registerTool("run_bash_command", {
title: "Run Bash Command",
description: "Executes a bash command in the workspace and returns the output.",
inputSchema: {
command: z.string().describe("The bash command to execute."),
cwd: z.string().default(WORKSPACE_DIR).describe("The current working directory to execute the command in."),
},
}, async ({ command, cwd }) => {
// Security: Ensure cwd is within WORKSPACE_DIR and resolve any '..'
const resolvedCwd = path.resolve(WORKSPACE_DIR, path.relative(WORKSPACE_DIR, cwd));
if (!resolvedCwd.startsWith(WORKSPACE_DIR)) {
return { content: [{ type: "text", text: `Error: Attempted to access directory outside of workspace.` }] };
}
// Construct command to run and then print the new CWD
const fullCommand = `cd "${resolvedCwd}" && ${command} && echo "${CWD_MARKER}:$(pwd)"`;
try {
const { stdout, stderr } = await execAsync(fullCommand, { cwd: WORKSPACE_DIR });
const output = stdout + (stderr ? `\n--- STDERR ---\n${stderr}` : '');
return { content: [{ type: "text", text: output }] };
} catch (e: any) {
// 'e' from exec contains stdout and stderr which are useful for capturing output from failing commands
const output = (e.stdout || '') + (e.stderr ? `\n--- STDERR ---\n${e.stderr}` : '');
const finalOutput = output.trim() ? output : `Execution failed: ${e.message}`;
return { content: [{ type: "text", text: finalOutput }] };
}
});
server.registerTool("github_get_status", {
title: "Get Git Repository Status",
description: "Checks if the workspace is a git repository and returns its status, including branch, remote URL, and changed files.",
inputSchema: {},
}, async () => {
try {
// Check if it's a git repo. This will throw if not.
await execAsync(`cd ${WORKSPACE_DIR} && git rev-parse --is-inside-work-tree`);
const { stdout: branch } = await execAsync(`cd ${WORKSPACE_DIR} && git rev-parse --abbrev-ref HEAD`);
const { stdout: remoteUrl } = await execAsync(`cd ${WORKSPACE_DIR} && git remote get-url origin`);
const { stdout: status } = await execAsync(`cd ${WORKSPACE_DIR} && git status --porcelain`);
const files = status.trim().split('\n').filter(Boolean);
const response = {
isRepo: true,
branch: branch.trim(),
remoteUrl: remoteUrl.trim(),
files: files,
};
return { content: [{ type: "text", text: JSON.stringify(response) }] };
} catch (e) {
// If any command fails, assume it's not a git repo or it's in a bad state.
return { content: [{ type: "text", text: JSON.stringify({ isRepo: false }) }] };
}
});
server.registerTool("github_commit_and_push", {
title: "Commit and Push to GitHub",
description: "Commits all changes in the workspace and pushes them to a specified branch.",
inputSchema: {
branch: z.string().describe("The branch to push to."),
commitMessage: z.string().describe("The commit message."),
authorName: z.string().describe("The author's name for the commit."),
authorEmail: z.string().describe("The author's email for the commit."),
},
}, async ({ branch, commitMessage, authorName, authorEmail }) => {
// The GITHUB_PAT is handled by the global git config set on startup.
const commands = [
`cd ${WORKSPACE_DIR}`,
`git config user.name "${authorName}"`,
`git config user.email "${authorEmail}"`,
'git add .',
`git commit -m "${commitMessage.replace(/"/g, '\\"')}"`,
`git push origin ${branch}`
].join(' && ');
try {
const { stdout, stderr } = await execAsync(commands);
return { content: [{ type: "text", text: `Successfully pushed to ${branch}.\n${stdout}\n${stderr}` }] };
} catch (e: any) {
return { content: [{ type: "text", text: `Commit and push failed: ${e.stderr || e.message}` }] };
}
});
return server.server;
}