Skip to main content
Glama

MTS MCP Server

by CalvinMagezi
package-manager.ts14.9 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, CallToolRequest, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js"; import { exec } from "child_process"; import { promisify } from "util"; import { mkdir } from 'fs/promises'; import { dirname } from 'path'; import path from 'path'; // Convert exec to promise-based const execAsync = promisify(exec); // Supported package managers and their commands const PACKAGE_MANAGERS = { npm: { install: "npm install", uninstall: "npm uninstall", installDev: "npm install --save-dev", list: "npm list --depth=0", }, yarn: { install: "yarn add", uninstall: "yarn remove", installDev: "yarn add --dev", list: "yarn list --depth=0", }, pnpm: { install: "pnpm add", uninstall: "pnpm remove", installDev: "pnpm add -D", list: "pnpm list --depth=0", }, bun: { install: "bun add", uninstall: "bun remove", installDev: "bun add -d", list: "bun list --depth=0", }, nextjs: { create: "bunx create-next-app@latest", }, }; // Validate package names to prevent command injection function isValidPackageName(name: string): boolean { return /^[@a-zA-Z0-9-_/.]+$/.test(name); } // Add this new function to handle interactive commands async function executeInteractiveCommand( command: string, inputs: Record<string, string> ): Promise<string> { return new Promise((resolve, reject) => { const child = exec(command); let output = ""; let currentQuestion = ""; // Handle command output child.stdout?.on("data", (data) => { const text = data.toString(); output += text; currentQuestion += text; // Check if we have an answer for the current question Object.entries(inputs).forEach(([question, answer]) => { if (currentQuestion.toLowerCase().includes(question.toLowerCase())) { child.stdin?.write(`${answer}\n`); currentQuestion = ""; } }); }); child.stderr?.on("data", (data) => { output += data.toString(); }); child.on("close", (code) => { if (code === 0) { resolve(output); } else { reject(new Error(`Command failed with code ${code}\n${output}`)); } }); // Handle any errors child.on("error", (error) => { reject(error); }); }); } // Add these constants at the top of the file const ALLOWED_DIRECTORIES = [ '/Users/calvinmagezi/Desktop', '/Users/calvinmagezi/Downloads', '/Users/calvinmagezi/Documents' ]; export class PackageManagerServer { private server: Server; private currentWorkingDir: string; private defaultWorkingDir: string; constructor() { this.server = new Server( { name: "package-manager-server", version: "0.1.0", }, { capabilities: { resources: {}, tools: {}, }, } ); // Set default working directory to desktop const homedir = process.env.HOME || process.env.USERPROFILE; this.defaultWorkingDir = path.join(homedir || '', 'desktop'); this.currentWorkingDir = this.defaultWorkingDir; this.setupHandlers(); this.setupErrorHandling(); } private setupErrorHandling(): void { this.server.onerror = (error) => { console.error("[MCP Error]", error); }; process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } private setupHandlers(): void { this.setupResourceHandlers(); this.setupToolHandlers(); } private setupResourceHandlers(): void { this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: "packages://installed", name: "Installed Packages", mimeType: "application/json", description: "List of currently installed packages in the project", }, ], })); this.server.setRequestHandler( ReadResourceRequestSchema, async (request) => { if (request.params.uri !== "packages://installed") { throw new McpError( ErrorCode.InvalidRequest, `Unknown resource: ${request.params.uri}` ); } try { // Try npm list first, fallback to other package managers if needed const { stdout } = await execAsync(PACKAGE_MANAGERS.npm.list, { cwd: this.currentWorkingDir, }); return { contents: [ { uri: request.params.uri, mimeType: "application/json", text: JSON.stringify({ packages: stdout }, null, 2), }, ], }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to list packages: ${error.message}` ); } } ); } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "manage_packages", description: "Install or uninstall packages using npm/yarn/pnpm/bun", inputSchema: { type: "object", properties: { operation: { type: "string", enum: ["install", "uninstall"], description: "Operation to perform", }, packages: { type: "array", items: { type: "string" }, description: "Package names", }, packageManager: { type: "string", enum: ["npm", "yarn", "pnpm", "bun"], default: "bun", description: "Package manager to use", }, dev: { type: "boolean", default: false, description: "Install as dev dependency", }, }, required: ["operation", "packages"], }, }, { name: "set_working_directory", description: "Set the working directory for package operations", inputSchema: { type: "object", properties: { path: { type: "string", description: "Path to working directory", }, }, required: ["path"], }, }, { name: "create_nextjs_app", description: "Create a new Next.js application with interactive setup", inputSchema: { type: "object", properties: { projectPath: { type: "string", description: "Path where the Next.js project should be created", }, answers: { type: "object", description: "Answers to the setup questions", properties: { "What is your project named?": { type: "string", description: "The name of your Next.js project", }, "Would you like to use TypeScript?": { type: "string", enum: ["No", "Yes"], default: "Yes", }, "Would you like to use ESLint?": { type: "string", enum: ["No", "Yes"], default: "Yes", }, "Would you like to use Tailwind CSS?": { type: "string", enum: ["No", "Yes"], default: "Yes", }, "Would you like your code inside a `src/` directory?": { type: "string", enum: ["No", "Yes"], default: "Yes", }, "Would you like to use App Router? (recommended)": { type: "string", enum: ["No", "Yes"], default: "Yes", }, "Would you like to use Turbopack for `next dev`?": { type: "string", enum: ["No", "Yes"], default: "No", }, "Would you like to customize the import alias ('*' (see below for file content) by default)?": { type: "string", enum: ["No", "Yes"], default: "No", }, }, required: ["What is your project named?"], }, }, required: ["projectPath", "answers"], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { return this.handleToolRequest( request.params.name, request.params.arguments || {} ); }); } private validateWorkingDirectory(dir: string): boolean { const normalizedPath = path.normalize(dir); return ALLOWED_DIRECTORIES.some(allowed => normalizedPath.toLowerCase().startsWith(allowed.toLowerCase()) ); } private async ensureWorkingDirectory(): Promise<void> { if (!this.validateWorkingDirectory(this.currentWorkingDir)) { this.currentWorkingDir = this.defaultWorkingDir; } await mkdir(this.currentWorkingDir, { recursive: true }); } public async handleToolRequest(toolName: string, params: Record<string, any>) { try { // Helper function to create standardized tool responses const createToolResponse = (content: { type: string; text: string }[]) => ({ _meta: {}, content, }); switch (toolName) { case "manage_packages": { const { operation, packages, packageManager = "bun", dev = false, } = params as { operation: "install" | "uninstall"; packages: string[]; packageManager?: keyof typeof PACKAGE_MANAGERS; dev?: boolean; }; // Validate all package names if (!packages.every(isValidPackageName)) { return createToolResponse([ { type: "text", text: "Invalid package name found. Package names can only contain letters, numbers, @, -, _, /, and .", }, ]); } try { const pm = PACKAGE_MANAGERS[packageManager as keyof typeof PACKAGE_MANAGERS]; if (!pm) { throw new Error( `Package manager ${packageManager} is not supported` ); } if (!("install" in pm)) { throw new Error( `Package manager ${packageManager} doesn't support this operation` ); } const command = operation === "install" ? dev ? pm.installDev : pm.install : pm.uninstall; const { stdout, stderr } = await execAsync( `${command} ${packages.join(" ")}`, { cwd: this.currentWorkingDir } ); return createToolResponse([ { type: "text", text: stdout + stderr, }, ]); } catch (error: any) { return createToolResponse([ { type: "text", text: `Command failed: ${error.message}`, }, ]); } } case "set_working_directory": { const { path: newPath } = params as { path: string }; if (!this.validateWorkingDirectory(newPath)) { throw new McpError( ErrorCode.InvalidRequest, `Access denied - path outside allowed directories: ${newPath}\nAllowed directories: ${ALLOWED_DIRECTORIES.join(', ')}` ); } this.currentWorkingDir = newPath; await this.ensureWorkingDirectory(); return createToolResponse([ { type: "text", text: `Working directory set to: ${newPath}`, }, ]); } case "create_nextjs_app": { const { projectPath, answers } = params as { projectPath: string; answers: Record<string, string>; }; try { // Validate and ensure working directory await this.ensureWorkingDirectory(); // Ensure project path is within allowed directories const fullProjectPath = path.isAbsolute(projectPath) ? projectPath : path.join(this.currentWorkingDir, projectPath); if (!this.validateWorkingDirectory(fullProjectPath)) { throw new McpError( ErrorCode.InvalidRequest, `Project path must be within allowed directories: ${ALLOWED_DIRECTORIES.join(', ')}` ); } // Create parent directory await mkdir(dirname(fullProjectPath), { recursive: true }); // Store original directory and change to project parent const originalDir = process.cwd(); process.chdir(dirname(fullProjectPath)); // Execute create-next-app const command = PACKAGE_MANAGERS.nextjs.create; const output = await executeInteractiveCommand(command, answers); // Restore original directory process.chdir(originalDir); return createToolResponse([ { type: "text", text: output, }, ]); } catch (error: any) { // Ensure we return to original directory even on error try { process.chdir(this.currentWorkingDir); } catch {} return createToolResponse([ { type: "text", text: `Failed to create Next.js app: ${error.message}`, }, ]); } } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${toolName}` ); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Error executing tool ${toolName}: ${(error as Error).message}` ); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/CalvinMagezi/mts-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server