index.ts•16 kB
// src/index.ts - MCP Server with SSE (Server-Sent Events) Transport
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import express from "express";
import fs from "fs/promises";
import { exec } from "child_process";
import { promisify } from "util";
import path from "path";
import cors from "cors";
const execAsync = promisify(exec);
const PROJECT_ROOT = process.env.PROJECT_ROOT || "/workspace";
const PORT = process.env.PORT || 3001;
const app = express();
app.use(cors());
app.use(express.json());
// Health check endpoint
app.get("/health", (req, res) => {
res.json({ status: "healthy", timestamp: new Date().toISOString() });
});
// Create MCP Server
const createMCPServer = () => {
const server = new Server(
{
name: "file-git-manager",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Register tool list handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "read_file",
description: "Read contents of a file from the project directory. Returns the full text content of the file.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Relative path to the file from project root (e.g., 'src/index.ts' or 'README.md')",
},
},
required: ["path"],
},
},
{
name: "write_file",
description: "Write or overwrite a file in the project directory. Creates parent directories if needed.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Relative path to the file from project root",
},
content: {
type: "string",
description: "Content to write to the file",
},
},
required: ["path", "content"],
},
},
{
name: "list_files",
description: "List files and directories in a given path. Can list recursively to show entire directory tree.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Relative path from project root (default: current directory)",
default: ".",
},
recursive: {
type: "boolean",
description: "List files recursively through subdirectories",
default: false,
},
},
},
},
{
name: "delete_file",
description: "Delete a file or directory (including all contents if directory)",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Relative path to the file/directory to delete",
},
},
required: ["path"],
},
},
{
name: "create_directory",
description: "Create a new directory (creates parent directories if needed)",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Relative path for the new directory",
},
},
required: ["path"],
},
},
{
name: "git_status",
description: "Get the current git status showing modified, staged, and untracked files",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "git_add",
description: "Stage files for the next commit. Use '.' to stage all changes.",
inputSchema: {
type: "object",
properties: {
files: {
type: "array",
items: { type: "string" },
description: "Array of file paths to stage, or ['.'] for all files",
},
},
required: ["files"],
},
},
{
name: "git_commit",
description: "Create a git commit with staged changes",
inputSchema: {
type: "object",
properties: {
message: {
type: "string",
description: "Commit message describing the changes",
},
},
required: ["message"],
},
},
{
name: "git_push",
description: "Push commits to remote repository",
inputSchema: {
type: "object",
properties: {
remote: {
type: "string",
description: "Remote name (default: origin)",
default: "origin",
},
branch: {
type: "string",
description: "Branch name (leave empty for current branch)",
},
},
},
},
{
name: "git_pull",
description: "Pull and merge changes from remote repository",
inputSchema: {
type: "object",
properties: {
remote: {
type: "string",
description: "Remote name (default: origin)",
default: "origin",
},
branch: {
type: "string",
description: "Branch name (leave empty for current branch)",
},
},
},
},
{
name: "git_log",
description: "Show recent commit history with graph visualization",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description: "Number of commits to show (default: 10)",
default: 10,
},
},
},
},
{
name: "git_diff",
description: "Show differences in files (unstaged or staged changes)",
inputSchema: {
type: "object",
properties: {
file: {
type: "string",
description: "Specific file to show diff for (optional, shows all if omitted)",
},
staged: {
type: "boolean",
description: "Show staged changes instead of unstaged",
default: false,
},
},
},
},
{
name: "search_in_files",
description: "Search for text pattern in files using grep",
inputSchema: {
type: "object",
properties: {
pattern: {
type: "string",
description: "Text pattern to search for",
},
path: {
type: "string",
description: "Directory to search in (default: current directory)",
default: ".",
},
filePattern: {
type: "string",
description: "File pattern to match (e.g., '*.js', '*.md')",
},
},
required: ["pattern"],
},
},
],
};
});
// Register tool execution handler
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (!args) {
throw new Error("Arguments are required");
}
try {
switch (name) {
case "read_file": {
const filePath = path.join(PROJECT_ROOT, args.path as string);
const content = await fs.readFile(filePath, "utf-8");
return {
content: [
{
type: "text",
text: content,
},
],
};
}
case "write_file": {
const filePath = path.join(PROJECT_ROOT, args.path as string);
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, args.content as string, "utf-8");
return {
content: [
{
type: "text",
text: `✅ File written successfully: ${args.path}`,
},
],
};
}
case "list_files": {
const dirPath = path.join(PROJECT_ROOT, (args.path as string) || ".");
const recursive = args.recursive as boolean;
const listFilesRecursive = async (dir: string, prefix = ""): Promise<string[]> => {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativePath = path.relative(PROJECT_ROOT, fullPath);
if (entry.isDirectory()) {
files.push(`${prefix}📁 ${entry.name}/`);
if (recursive) {
const subFiles = await listFilesRecursive(fullPath, prefix + " ");
files.push(...subFiles);
}
} else {
files.push(`${prefix}📄 ${entry.name}`);
}
}
return files;
};
const files = await listFilesRecursive(dirPath);
return {
content: [
{
type: "text",
text: files.length > 0 ? files.join("\n") : "Directory is empty",
},
],
};
}
case "delete_file": {
const filePath = path.join(PROJECT_ROOT, args.path as string);
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
await fs.rm(filePath, { recursive: true, force: true });
} else {
await fs.unlink(filePath);
}
return {
content: [
{
type: "text",
text: `✅ Deleted: ${args.path}`,
},
],
};
}
case "create_directory": {
const dirPath = path.join(PROJECT_ROOT, args.path as string);
await fs.mkdir(dirPath, { recursive: true });
return {
content: [
{
type: "text",
text: `✅ Directory created: ${args.path}`,
},
],
};
}
case "git_status": {
const { stdout } = await execAsync("git status", { cwd: PROJECT_ROOT });
return {
content: [
{
type: "text",
text: stdout,
},
],
};
}
case "git_add": {
const files = (args.files as string[]).join(" ");
const { stdout, stderr } = await execAsync(`git add ${files}`, {
cwd: PROJECT_ROOT,
});
return {
content: [
{
type: "text",
text: `✅ Files staged: ${files}\n${stdout}${stderr}`,
},
],
};
}
case "git_commit": {
const message = (args.message as string).replace(/"/g, '\\"');
const { stdout } = await execAsync(`git commit -m "${message}"`, {
cwd: PROJECT_ROOT,
});
return {
content: [
{
type: "text",
text: `✅ Commit created:\n${stdout}`,
},
],
};
}
case "git_push": {
const remote = (args.remote as string) || "origin";
const branch = args.branch ? ` ${args.branch}` : "";
const { stdout, stderr } = await execAsync(`git push ${remote}${branch}`, {
cwd: PROJECT_ROOT,
});
return {
content: [
{
type: "text",
text: `✅ Pushed to ${remote}:\n${stdout}${stderr}`,
},
],
};
}
case "git_pull": {
const remote = (args.remote as string) || "origin";
const branch = args.branch ? ` ${args.branch}` : "";
const { stdout, stderr } = await execAsync(`git pull ${remote}${branch}`, {
cwd: PROJECT_ROOT,
});
return {
content: [
{
type: "text",
text: `✅ Pulled from ${remote}:\n${stdout}${stderr}`,
},
],
};
}
case "git_log": {
const limit = (args.limit as number) || 10;
const { stdout } = await execAsync(
`git log --oneline --graph --decorate -n ${limit}`,
{ cwd: PROJECT_ROOT }
);
return {
content: [
{
type: "text",
text: stdout || "No commits yet",
},
],
};
}
case "git_diff": {
const file = args.file ? ` ${args.file}` : "";
const staged = args.staged ? " --staged" : "";
const { stdout } = await execAsync(`git diff${staged}${file}`, {
cwd: PROJECT_ROOT,
});
return {
content: [
{
type: "text",
text: stdout || "No changes",
},
],
};
}
case "search_in_files": {
const pattern = args.pattern as string;
const searchPath = (args.path as string) || ".";
const filePattern = args.filePattern ? `--include="${args.filePattern}"` : "";
try {
const { stdout } = await execAsync(
`grep -rn ${filePattern} "${pattern}" ${searchPath}`,
{ cwd: PROJECT_ROOT }
);
return {
content: [
{
type: "text",
text: stdout || "No matches found",
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: "No matches found",
},
],
};
}
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return {
content: [
{
type: "text",
text: `❌ Error: ${error.message}`,
},
],
isError: true,
};
}
});
return server;
};
// SSE endpoint for MCP
app.post("/sse", async (req, res) => {
console.log("📡 New SSE connection request");
const server = createMCPServer();
const transport = new SSEServerTransport("/message", res);
await server.connect(transport);
console.log("✅ MCP Server connected via SSE");
});
// Message endpoint for SSE
app.post("/message", async (req, res) => {
console.log("📨 Received message:", req.body);
res.json({ received: true });
});
// Start server
app.listen(PORT, () => {
console.log(`🚀 MCP Server running on port ${PORT}`);
console.log(`📍 SSE endpoint: http://localhost:${PORT}/sse`);
console.log(`📍 Health check: http://localhost:${PORT}/health`);
console.log(`📁 Project root: ${PROJECT_ROOT}`);
});