https://github.com/Streen9/react-mcp
by Streen9
Verified
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 { z } from "zod";
import { spawn, exec } from "child_process";
import fs from "fs";
import path from "path";
import os from "os";
// Initialize logging
const LOG_DIR = "logs";
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR);
}
const getCurrentTimestamp = () => {
return new Date().toISOString().replace(/[:.]/g, "-");
};
const logToFile = (data, type = "json") => {
const timestamp = getCurrentTimestamp();
const logEntry = {
timestamp,
...data,
};
// JSON logging
const jsonLogPath = path.join(LOG_DIR, "react-mcp-logs.json");
let jsonLogs = [];
if (fs.existsSync(jsonLogPath)) {
const fileContent = fs.readFileSync(jsonLogPath, "utf8");
jsonLogs = fileContent ? JSON.parse(fileContent) : [];
}
jsonLogs.push(logEntry);
fs.writeFileSync(jsonLogPath, JSON.stringify(jsonLogs, null, 2));
// Text logging
const txtLogPath = path.join(LOG_DIR, "react-mcp-logs.txt");
const txtLogEntry = `[${timestamp}] ${JSON.stringify(data)}\n`;
fs.appendFileSync(txtLogPath, txtLogEntry);
};
// Keep track of running processes
const runningProcesses = new Map();
// Execute terminal commands
async function executeCommand(command, options = {}) {
return new Promise((resolve, reject) => {
exec(command, options, (error, stdout, stderr) => {
if (error) {
return reject({ error, stderr });
}
resolve({ stdout, stderr });
});
});
}
// Start a long-running process and return its output stream
function startProcess(command, args, cwd) {
const childProcess = spawn(command, args, {
cwd,
shell: true,
env: { ...process.env, FORCE_COLOR: "true" },
});
let output = "";
let errorOutput = "";
childProcess.stdout.on("data", (data) => {
const chunk = data.toString();
output += chunk;
});
childProcess.stderr.on("data", (data) => {
const chunk = data.toString();
errorOutput += chunk;
});
const processId = Math.random().toString(36).substring(2, 15);
runningProcesses.set(processId, {
process: childProcess,
command,
args,
cwd,
output,
errorOutput,
startTime: new Date(),
processId,
});
return processId;
}
// Tool handlers
async function handleCreateReactApp(params) {
try {
const { name, template, directory } = params;
if (!name) {
throw new Error("Project name is required");
}
// Determine base directory
const baseDir = directory || os.homedir();
const projectDir = path.join(baseDir, name);
// Check if directory already exists
if (fs.existsSync(projectDir)) {
throw new Error(`Directory ${projectDir} already exists`);
}
// Prepare create-react-app command
const createCommand = template
? `npx create-react-app ${name} --template ${template}`
: `npx create-react-app ${name}`;
console.log(
`Creating React app in ${baseDir} with command: ${createCommand}`
);
// Run the command
const processId = startProcess(createCommand, [], baseDir);
return {
message: `Creating React app "${name}" in ${projectDir}`,
processId: processId,
projectDir: projectDir,
};
} catch (error) {
return {
error: `Error creating React app: ${error.message}`,
};
}
}
async function handleRunReactApp(params) {
try {
const { projectPath } = params;
if (!projectPath) {
throw new Error("Project path is required");
}
// Check if directory exists
if (!fs.existsSync(projectPath)) {
throw new Error(`Directory ${projectPath} does not exist`);
}
// Check if it's a React app (package.json exists with react dependency)
const packageJsonPath = path.join(projectPath, "package.json");
if (!fs.existsSync(packageJsonPath)) {
throw new Error(
`Not a valid React app: package.json not found in ${projectPath}`
);
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
if (!packageJson.dependencies || !packageJson.dependencies.react) {
throw new Error(
`Not a valid React app: react dependency not found in package.json`
);
}
// Start the development server
const processId = startProcess("npm", ["start"], projectPath);
return {
message: `Starting React development server in ${projectPath}`,
processId: processId,
note: "The development server should be accessible at http://localhost:3000",
};
} catch (error) {
return {
error: `Error running React app: ${error.message}`,
};
}
}
async function handleRunCommand(params) {
try {
const { command, directory } = params;
if (!command) {
throw new Error("Command is required");
}
// Determine directory
const workingDir = directory || process.cwd();
// Check if directory exists
if (!fs.existsSync(workingDir)) {
throw new Error(`Directory ${workingDir} does not exist`);
}
// Run the command
const result = await executeCommand(command, { cwd: workingDir });
return {
command: command,
directory: workingDir,
output: result.stdout,
stderr: result.stderr || "",
};
} catch (error) {
return {
error: `Error executing command: ${error.message}`,
stderr: error.stderr || "",
};
}
}
async function handleGetProcessOutput(params) {
try {
const { processId } = params;
if (!processId) {
throw new Error("Process ID is required");
}
if (!runningProcesses.has(processId)) {
throw new Error(`Process with ID ${processId} not found`);
}
const processInfo = runningProcesses.get(processId);
const isRunning = processInfo.process.exitCode === null;
return {
processId: processId,
command: `${processInfo.command} ${processInfo.args.join(" ")}`,
directory: processInfo.cwd,
isRunning: isRunning,
exitCode: processInfo.process.exitCode,
output: processInfo.output,
errorOutput: processInfo.errorOutput,
startTime: processInfo.startTime.toISOString(),
runTime: `${Math.floor(
(new Date() - processInfo.startTime) / 1000
)} seconds`,
};
} catch (error) {
return {
error: `Error getting process output: ${error.message}`,
};
}
}
async function handleStopProcess(params) {
try {
const { processId } = params;
if (!processId) {
throw new Error("Process ID is required");
}
if (!runningProcesses.has(processId)) {
throw new Error(`Process with ID ${processId} not found`);
}
const processInfo = runningProcesses.get(processId);
// Kill the process
processInfo.process.kill();
return {
message: `Process ${processId} stopped`,
command: `${processInfo.command} ${processInfo.args.join(" ")}`,
directory: processInfo.cwd,
};
} catch (error) {
return {
error: `Error stopping process: ${error.message}`,
};
}
}
async function handleListProcesses() {
try {
const processes = [];
for (const [processId, processInfo] of runningProcesses.entries()) {
const isRunning = processInfo.process.exitCode === null;
processes.push({
processId: processId,
command: `${processInfo.command} ${processInfo.args.join(" ")}`,
directory: processInfo.cwd,
isRunning: isRunning,
exitCode: processInfo.process.exitCode,
startTime: processInfo.startTime.toISOString(),
runTime: `${Math.floor(
(new Date() - processInfo.startTime) / 1000
)} seconds`,
});
}
return {
processes: processes,
count: processes.length,
};
} catch (error) {
return {
error: `Error listing processes: ${error.message}`,
};
}
}
async function handleEditFile(params) {
try {
const { filePath, content } = params;
if (!filePath) {
throw new Error("File path is required");
}
if (content === undefined || content === null) {
throw new Error("File content is required");
}
// Make sure directory exists
const directory = path.dirname(filePath);
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
// Write content to file
fs.writeFileSync(filePath, content, "utf8");
return {
message: `File ${filePath} updated successfully`,
filePath: filePath,
size: Buffer.byteLength(content, "utf8"),
};
} catch (error) {
return {
error: `Error editing file: ${error.message}`,
};
}
}
async function handleReadFile(params) {
try {
const { filePath } = params;
if (!filePath) {
throw new Error("File path is required");
}
// Check if file exists
if (!fs.existsSync(filePath)) {
throw new Error(`File ${filePath} does not exist`);
}
// Read file content
const content = fs.readFileSync(filePath, "utf8");
return {
filePath: filePath,
content: content,
size: Buffer.byteLength(content, "utf8"),
};
} catch (error) {
return {
error: `Error reading file: ${error.message}`,
};
}
}
async function handleInstallPackage(params) {
try {
const { packageName, directory, dev } = params;
if (!packageName) {
throw new Error("Package name is required");
}
// Determine directory
const workingDir = directory || process.cwd();
// Check if directory exists
if (!fs.existsSync(workingDir)) {
throw new Error(`Directory ${workingDir} does not exist`);
}
// Check if package.json exists
const packageJsonPath = path.join(workingDir, "package.json");
if (!fs.existsSync(packageJsonPath)) {
throw new Error(
`Not a valid Node.js project: package.json not found in ${workingDir}`
);
}
// Install the package
const installCommand = dev
? `npm install ${packageName} --save-dev`
: `npm install ${packageName}`;
const processId = startProcess(installCommand, [], workingDir);
return {
message: `Installing ${packageName} in ${workingDir}`,
processId: processId,
command: installCommand,
};
} catch (error) {
return {
error: `Error installing package: ${error.message}`,
};
}
}
// Server setup
const server = new Server(
{
name: "react-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Define schemas
const CreateReactAppSchema = z.object({
name: z.string(),
template: z.string().optional(),
directory: z.string().optional(),
});
const RunReactAppSchema = z.object({
projectPath: z.string(),
});
const RunCommandSchema = z.object({
command: z.string(),
directory: z.string().optional(),
});
const GetProcessOutputSchema = z.object({
processId: z.string(),
});
const StopProcessSchema = z.object({
processId: z.string(),
});
const EditFileSchema = z.object({
filePath: z.string(),
content: z.string(),
});
const ReadFileSchema = z.object({
filePath: z.string(),
});
const InstallPackageSchema = z.object({
packageName: z.string(),
directory: z.string().optional(),
dev: z.boolean().optional(),
});
// Tool request handler
server.setRequestHandler(ListToolsRequestSchema, async () => {
const response = {
tools: [
{
name: "create-react-app",
description: "Create a new React application",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the React app",
},
template: {
type: "string",
description:
"Template to use (e.g., typescript, cra-template-pwa)",
},
directory: {
type: "string",
description:
"Base directory to create the app in (defaults to home directory)",
},
},
required: ["name"],
},
},
{
name: "run-react-app",
description: "Run a React application in development mode",
inputSchema: {
type: "object",
properties: {
projectPath: {
type: "string",
description: "Path to the React project folder",
},
},
required: ["projectPath"],
},
},
{
name: "run-command",
description: "Run a terminal command",
inputSchema: {
type: "object",
properties: {
command: {
type: "string",
description: "Command to execute",
},
directory: {
type: "string",
description:
"Directory to run the command in (defaults to current directory)",
},
},
required: ["command"],
},
},
{
name: "get-process-output",
description: "Get the output from a running or completed process",
inputSchema: {
type: "object",
properties: {
processId: {
type: "string",
description: "ID of the process to get output from",
},
},
required: ["processId"],
},
},
{
name: "stop-process",
description: "Stop a running process",
inputSchema: {
type: "object",
properties: {
processId: {
type: "string",
description: "ID of the process to stop",
},
},
required: ["processId"],
},
},
{
name: "list-processes",
description: "List all running processes",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "edit-file",
description: "Create or edit a file",
inputSchema: {
type: "object",
properties: {
filePath: {
type: "string",
description: "Path to the file to edit",
},
content: {
type: "string",
description: "Content to write to the file",
},
},
required: ["filePath", "content"],
},
},
{
name: "read-file",
description: "Read the contents of a file",
inputSchema: {
type: "object",
properties: {
filePath: {
type: "string",
description: "Path to the file to read",
},
},
required: ["filePath"],
},
},
{
name: "install-package",
description: "Install a npm package in a project",
inputSchema: {
type: "object",
properties: {
packageName: {
type: "string",
description:
"Name of the package to install (can include version)",
},
directory: {
type: "string",
description:
"Directory of the project (defaults to current directory)",
},
dev: {
type: "boolean",
description: "Whether to install as a dev dependency",
},
},
required: ["packageName"],
},
},
],
};
logToFile({ event: "list_tools", response }, "json");
return response;
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request, context) => {
const { name, arguments: args } = request.params;
logToFile({ event: "call_tool", name, args }, "json");
try {
let result;
switch (name) {
case "create-react-app":
const createArgs = CreateReactAppSchema.parse(args);
result = await handleCreateReactApp(createArgs);
break;
case "run-react-app":
const runArgs = RunReactAppSchema.parse(args);
result = await handleRunReactApp(runArgs);
break;
case "run-command":
const commandArgs = RunCommandSchema.parse(args);
result = await handleRunCommand(commandArgs);
break;
case "get-process-output":
const outputArgs = GetProcessOutputSchema.parse(args);
result = await handleGetProcessOutput(outputArgs);
break;
case "stop-process":
const stopArgs = StopProcessSchema.parse(args);
result = await handleStopProcess(stopArgs);
break;
case "list-processes":
result = await handleListProcesses();
break;
case "edit-file":
const editArgs = EditFileSchema.parse(args);
result = await handleEditFile(editArgs);
break;
case "read-file":
const readArgs = ReadFileSchema.parse(args);
result = await handleReadFile(readArgs);
break;
case "install-package":
const installArgs = InstallPackageSchema.parse(args);
result = await handleInstallPackage(installArgs);
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
return createTextResponse(JSON.stringify(result, null, 2));
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Invalid arguments: ${error.errors
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join(", ")}`
);
}
throw error;
}
});
// Start the server
const transport = new StdioServerTransport();
server.connect(transport).then(() => {
console.error("React MCP Server running on stdio");
});
const createTextResponse = (text) => ({
content: [{ type: "text", text }],
});
// Clean up processes on exit
process.on("exit", () => {
for (const [processId, processInfo] of runningProcesses.entries()) {
try {
processInfo.process.kill();
} catch (error) {
console.error(`Failed to kill process ${processId}:`, error);
}
}
});
process.on("SIGINT", () => {
process.exit(0);
});
process.on("SIGTERM", () => {
process.exit(0);
});