import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { execa } from "execa";
import simpleGit from "simple-git";
import { promises as fs } from "fs";
import path from "path";
import os from "os";
// Initialize git
const git = simpleGit();
// Helper function to ensure directory exists
async function ensureDirectory(dirPath: string): Promise<void> {
try {
await fs.mkdir(dirPath, { recursive: true });
} catch (error) {
console.error(`Error creating directory ${dirPath}:`, error);
}
}
// Helper function to resolve path (handles ~ for home directory)
function resolvePath(inputPath: string): string {
if (inputPath.startsWith("~")) {
return path.join(os.homedir(), inputPath.slice(1));
}
return path.resolve(inputPath);
}
// Helper function to open project in VSCode
async function openInVSCode(projectPath: string): Promise<void> {
try {
await execa("code", [projectPath]);
} catch (error) {
// If 'code' command fails, try common VSCode executable paths
const vscodePaths = [
"code",
"/usr/local/bin/code",
"/usr/bin/code",
"C:\\Program Files\\Microsoft VS Code\\Code.exe",
"C:\\Program Files (x86)\\Microsoft VS Code\\Code.exe",
];
for (const codePath of vscodePaths) {
try {
await execa(codePath, [projectPath]);
return;
} catch {
// Continue to next path
}
}
throw new Error("VSCode not found. Please ensure VSCode is installed and 'code' command is available in PATH");
}
}
// Create server instance
const server = new Server(
{
name: "mcp-git-terminal",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Define available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "execute_command",
description: "Execute a terminal command in a specified directory",
inputSchema: {
type: "object",
properties: {
command: {
type: "string",
description: "The command to execute (e.g., 'npm install', 'ls -la')",
},
cwd: {
type: "string",
description: "Working directory for the command (defaults to current directory)",
},
},
required: ["command"],
},
},
{
name: "git_clone",
description: "Clone a git repository to a specified location",
inputSchema: {
type: "object",
properties: {
repositoryUrl: {
type: "string",
description: "Git repository URL",
},
destination: {
type: "string",
description: "Destination path where to clone the repository",
},
openInVSCode: {
type: "boolean",
description: "Open the cloned repository in VSCode (default: false)",
},
},
required: ["repositoryUrl", "destination"],
},
},
{
name: "install_react_project",
description: "Create a new React project using Vite and open it in VSCode",
inputSchema: {
type: "object",
properties: {
projectName: {
type: "string",
description: "Name of the React project",
},
destination: {
type: "string",
description: "Directory where to create the project (e.g., ~/Desktop)",
},
template: {
type: "string",
description: "Vite template to use",
enum: ["react", "react-ts", "react-swc", "react-swc-ts"],
},
installDependencies: {
type: "boolean",
description: "Install dependencies after creating project (default: true)",
},
},
required: ["projectName", "destination"],
},
},
{
name: "install_vue_project",
description: "Create a new Vue project using Vite and open it in VSCode",
inputSchema: {
type: "object",
properties: {
projectName: {
type: "string",
description: "Name of the Vue project",
},
destination: {
type: "string",
description: "Directory where to create the project (e.g., ~/Desktop)",
},
template: {
type: "string",
description: "Vite template to use",
enum: ["vue", "vue-ts"],
},
installDependencies: {
type: "boolean",
description: "Install dependencies after creating project (default: true)",
},
},
required: ["projectName", "destination"],
},
},
{
name: "install_next_project",
description: "Create a new Next.js project and open it in VSCode",
inputSchema: {
type: "object",
properties: {
projectName: {
type: "string",
description: "Name of the Next.js project",
},
destination: {
type: "string",
description: "Directory where to create the project (e.g., ~/Desktop)",
},
typescript: {
type: "boolean",
description: "Use TypeScript (default: true)",
},
installDependencies: {
type: "boolean",
description: "Install dependencies after creating project (default: true)",
},
},
required: ["projectName", "destination"],
},
},
{
name: "open_in_vscode",
description: "Open a directory or file in VSCode",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Path to open in VSCode",
},
},
required: ["path"],
},
},
{
name: "check_directory",
description: "Check if a directory exists and list its contents",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Directory path to check",
},
},
required: ["path"],
},
},
],
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "execute_command": {
const { command, cwd } = args as { command: string; cwd?: string };
const workingDir = cwd ? resolvePath(cwd) : process.cwd();
// Execute command
const { stdout, stderr } = await execa(command, {
shell: true,
cwd: workingDir,
});
return {
content: [
{
type: "text",
text: `Command executed successfully:\n\nOutput:\n${stdout}\n${stderr ? `\nErrors/Warnings:\n${stderr}` : ""}`,
},
],
};
}
case "git_clone": {
const { repositoryUrl, destination, openInVSCode: shouldOpenInVSCode = false } = args as {
repositoryUrl: string;
destination: string;
openInVSCode?: boolean;
};
const destPath = resolvePath(destination);
await ensureDirectory(path.dirname(destPath));
await git.clone(repositoryUrl, destPath);
if (shouldOpenInVSCode) {
await openInVSCode(destPath);
}
return {
content: [
{
type: "text",
text: `Successfully cloned ${repositoryUrl} to ${destPath}${shouldOpenInVSCode ? " and opened in VSCode" : ""}`,
},
],
};
}
case "install_react_project": {
const {
projectName,
destination,
template = "react-ts",
installDependencies = true
} = args as {
projectName: string;
destination: string;
template?: string;
installDependencies?: boolean;
};
const destPath = resolvePath(destination);
await ensureDirectory(destPath);
// Create Vite React project
const createCommand = `npm create vite@latest ${projectName} -- --template ${template}`;
const { stdout: createOutput } = await execa(createCommand, {
shell: true,
cwd: destPath,
});
const projectPath = path.join(destPath, projectName);
// Install dependencies if requested
if (installDependencies) {
const installCommand = "npm install";
const { stdout: installOutput } = await execa(installCommand, {
shell: true,
cwd: projectPath,
});
}
// Open in VSCode
await openInVSCode(projectPath);
return {
content: [
{
type: "text",
text: `React project "${projectName}" created successfully with Vite at ${projectPath}\n\n` +
`Template: ${template}\n` +
`Dependencies: ${installDependencies ? "Installed" : "Not installed (run npm install manually)"}\n` +
`VSCode: Opened\n\n` +
`To start development:\n` +
` cd ${projectPath}\n` +
` ${installDependencies ? "" : "npm install\n "}npm run dev`,
},
],
};
}
case "install_vue_project": {
const {
projectName,
destination,
template = "vue-ts",
installDependencies = true
} = args as {
projectName: string;
destination: string;
template?: string;
installDependencies?: boolean;
};
const destPath = resolvePath(destination);
await ensureDirectory(destPath);
// Create Vite Vue project
const createCommand = `npm create vite@latest ${projectName} -- --template ${template}`;
const { stdout: createOutput } = await execa(createCommand, {
shell: true,
cwd: destPath,
});
const projectPath = path.join(destPath, projectName);
// Install dependencies if requested
if (installDependencies) {
const installCommand = "npm install";
const { stdout: installOutput } = await execa(installCommand, {
shell: true,
cwd: projectPath,
});
}
// Open in VSCode
await openInVSCode(projectPath);
return {
content: [
{
type: "text",
text: `Vue project "${projectName}" created successfully with Vite at ${projectPath}\n\n` +
`Template: ${template}\n` +
`Dependencies: ${installDependencies ? "Installed" : "Not installed (run npm install manually)"}\n` +
`VSCode: Opened\n\n` +
`To start development:\n` +
` cd ${projectPath}\n` +
` ${installDependencies ? "" : "npm install\n "}npm run dev`,
},
],
};
}
case "install_next_project": {
const {
projectName,
destination,
typescript = true,
installDependencies = true
} = args as {
projectName: string;
destination: string;
typescript?: boolean;
installDependencies?: boolean;
};
const destPath = resolvePath(destination);
await ensureDirectory(destPath);
const tsFlag = typescript ? "--typescript" : "--javascript";
const skipInstallFlag = installDependencies ? "" : "--skip-install";
const command = `npx create-next-app@latest ${projectName} ${tsFlag} --tailwind --app --no-git ${skipInstallFlag}`;
const { stdout, stderr } = await execa(command, {
shell: true,
cwd: destPath,
});
const projectPath = path.join(destPath, projectName);
// Open in VSCode
await openInVSCode(projectPath);
return {
content: [
{
type: "text",
text: `Next.js project "${projectName}" created successfully at ${projectPath}\n\n` +
`TypeScript: ${typescript ? "Yes" : "No"}\n` +
`Dependencies: ${installDependencies ? "Installed" : "Not installed (run npm install manually)"}\n` +
`VSCode: Opened\n\n` +
`To start development:\n` +
` cd ${projectPath}\n` +
` ${installDependencies ? "" : "npm install\n "}npm run dev`,
},
],
};
}
case "open_in_vscode": {
const { path: targetPath } = args as { path: string };
const resolvedPath = resolvePath(targetPath);
await openInVSCode(resolvedPath);
return {
content: [
{
type: "text",
text: `Opened ${resolvedPath} in VSCode`,
},
],
};
}
case "check_directory": {
const { path: dirPath } = args as { path: string };
const resolvedPath = resolvePath(dirPath);
try {
const stats = await fs.stat(resolvedPath);
if (!stats.isDirectory()) {
return {
content: [
{
type: "text",
text: `${resolvedPath} exists but is not a directory`,
},
],
};
}
const files = await fs.readdir(resolvedPath);
return {
content: [
{
type: "text",
text: `Directory ${resolvedPath} exists and contains:\n${files.join("\n")}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Directory ${resolvedPath} does not exist`,
},
],
};
}
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
});
// Start the server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Git & Terminal Server running on stdio");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});