# Directory Structure ``` ├── .gitignore ├── index.ts ├── package.json ├── README.md ├── src │ ├── handlers │ │ └── directory.ts │ ├── schemas │ │ └── directory.ts │ ├── types │ │ └── index.ts │ └── utils │ ├── command-executor.ts │ └── constants.ts ├── tsconfig.json └── yarn.lock ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ yarn-debug.log* yarn-error.log* npm-debug.log* # Build outputs dist/ build/ *.js *.js.map *.d.ts # Environment variables .env .env.local .env.*.local # IDE .vscode/ .idea/ *.sublime-project *.sublime-workspace # OS .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # MCP Terminal Use A Model Context Protocol (MCP) server for terminal access. This server allows Claude to interact with specified directories on your system. ## Configuration ### Environment Variables The server can be configured using the following environment variables: - `ALLOWED_DIRECTORY`: The directory path that Claude is allowed to access (default: '${HOME}/your/allowed/directory') You can set these variables either through: 1. A `.env` file in your project root 2. Environment variables in your system 3. Direct configuration in claude_desktop_config.json ### Claude Desktop Configuration Add this to your `claude_desktop_config.json`: ```json { "mcpServers": { "terminal": { "command": "node", "args": [ "${HOME}/path/to/mcp-terminal-use/dist/index.js" ], "env": { "ALLOWED_DIRECTORY": "${HOME}/your/allowed/directory" } } } } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, "outDir": "./dist", "rootDir": ".", "declaration": true, "resolveJsonModule": true }, "include": [ "./**/*.ts" ], "exclude": [ "node_modules", "dist" ] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "terminal", "version": "0.5.1", "description": "MCP server for terminal access zh", "license": "MIT", "author": "Alex Man", "homepage": "", "bugs": "", "type": "module", "bin": { "mcp-terminal": "dist/index.js" }, "files": [ "dist" ], "scripts": { "build": "tsc && shx chmod +x dist/*.js", "prepare": "npm run build", "watch": "tsc --watch" }, "dependencies": { "@modelcontextprotocol/sdk": "0.5.0", "dotenv": "^16.4.7", "glob": "^10.3.10", "replicate": "^0.27.1", "zod-to-json-schema": "^3.23.5" }, "devDependencies": { "@types/node": "^20.11.0", "shx": "^0.3.4", "typescript": "^5.3.3" } } ``` -------------------------------------------------------------------------------- /src/schemas/directory.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; import { isWithinAllowedDirectory, resolvePath, ALLOWED_DIRECTORY } from '../utils/constants.js'; export const MkdirSchema = z.object({ path: z.string().refine( (val) => isWithinAllowedDirectory(val), 'Directory must be created within allowed directory' ), }); export const CdSchema = z.object({ path: z.string().refine( (val) => { const targetPath = resolvePath(val); return targetPath.startsWith(ALLOWED_DIRECTORY); }, 'Can only change to directories within allowed directory' ), }); // Types derived from schemas export type MkdirArgs = z.infer<typeof MkdirSchema>; export type CdArgs = z.infer<typeof CdSchema>; // Validation functions export function validateMkdirArgs(args: unknown) { return MkdirSchema.safeParse(args); } export function validateCdArgs(args: unknown) { return CdSchema.safeParse(args); } ``` -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- ```typescript import { z } from 'zod'; // Base types for tool responses export interface ToolMeta { progressToken: null; } export interface ToolContent { type: string; text: string; } export interface ToolResponse { meta: ToolMeta; content: ToolContent[]; isError?: boolean; } // Generic handler type export type ToolHandler = (args: unknown) => Promise<ToolResponse>; // Schema validation result type export type ValidationResult<T> = { success: true; data: T; } | { success: false; error: z.ZodError; }; // Package manager types export interface PackageManagerOptions { packages?: string[]; flags?: string[]; dev?: boolean; } // Testing types export interface TestOptions { testPath?: string; watch?: boolean; mode?: 'run' | 'open'; spec?: string; } // Linting types export interface LintOptions { path?: string; fix?: boolean; write?: boolean; project?: string; } // Directory types export interface DirectoryOptions { path: string; } // Git types export interface GitOptions { path?: string; message?: string; patch?: string; staged?: boolean; } ``` -------------------------------------------------------------------------------- /src/handlers/directory.ts: -------------------------------------------------------------------------------- ```typescript import { ToolResponse } from '../types/index.js'; import { validateMkdirArgs, validateCdArgs } from '../schemas/directory.js'; import { createToolResponse, createErrorResponse, execAsync } from '../utils/command-executor.js'; export async function handleMkdir(args: unknown): Promise<ToolResponse> { try { const parsed = validateMkdirArgs(args); if (!parsed.success) { throw new Error(`Invalid arguments for mkdir: ${parsed.error}`); } const { path: dirPath } = parsed.data; const { stdout, stderr } = await execAsync(`mkdir -p ${dirPath}`); return createToolResponse(stdout || "Directory created successfully", stderr); } catch (error) { return createErrorResponse(error); } } export async function handleCd(args: unknown): Promise<ToolResponse> { try { const parsed = validateCdArgs(args); if (!parsed.success) { throw new Error(`Invalid arguments for cd: ${parsed.error}`); } const { path: dirPath } = parsed.data; try { process.chdir(dirPath); return createToolResponse(`Changed directory to: ${process.cwd()}`); } catch (err) { const error = err as Error; throw new Error(`Failed to change directory: ${error.message}`); } } catch (error) { return createErrorResponse(error); } } ``` -------------------------------------------------------------------------------- /src/utils/command-executor.ts: -------------------------------------------------------------------------------- ```typescript import { exec, spawn } from 'child_process'; import { promisify } from 'util'; // Promisify exec for async/await usage export const execAsync = promisify(exec); // Types for command execution results export interface CommandResult { stdout: string; stderr: string; } export interface ToolResponse { meta: { progressToken: null; }; content: Array<{ type: string; text: string; }>; isError?: boolean; } // Helper function to create a standard tool response export function createToolResponse(stdout: string, stderr?: string): ToolResponse { return { meta: { progressToken: null, }, content: [ { type: "text", text: stdout }, ...(stderr ? [{ type: "text", text: `Error: ${stderr}` }] : []), ], }; } // Helper function to create an error response export function createErrorResponse(error: unknown): ToolResponse { const errorMessage = error instanceof Error ? error.message : String(error); return { meta: { progressToken: null, }, content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true, }; } // Helper function to handle git apply with patch data export async function gitApplyWithPatch(patch: string): Promise<CommandResult> { return new Promise((resolve, reject) => { const git = spawn('git', ['apply']); let stdout = ''; let stderr = ''; git.stdout.on('data', (data) => { stdout += data.toString(); }); git.stderr.on('data', (data) => { stderr += data.toString(); }); git.on('close', (code) => { if (code === 0) { resolve({ stdout, stderr }); } else { reject(new Error(`git apply failed with code ${code}\n${stderr}`)); } }); git.stdin.write(patch); git.stdin.end(); }); } // Helper function to execute a command and return a tool response export async function executeCommand(command: string): Promise<ToolResponse> { try { const { stdout, stderr } = await execAsync(command); return createToolResponse(stdout, stderr); } catch (error) { return createErrorResponse(error); } } ``` -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- ```typescript import path from 'path'; import dotenv from 'dotenv'; dotenv.config(); // Allowed directory for command execution export const ALLOWED_DIRECTORY = process.env.ALLOWED_DIRECTORY as string // List of allowed commands export const ALLOWED_COMMANDS = [ // Git commands 'git diff', 'git diff --staged', 'git apply', 'git add -p', 'git init', 'git add', 'git commit', 'git status', 'git log', // Directory commands 'mkdir', 'cd', // NPM commands 'npm init', 'npm init -y', 'npm install', 'npm run', 'npm add', 'npm remove', 'npm create', // Yarn commands 'yarn init', 'yarn init -y', 'yarn install', 'yarn run', 'yarn add', 'yarn remove', 'yarn create', // Testing commands 'jest', 'vitest', 'cypress', // Linting and formatting 'eslint', 'prettier', 'tsc', // File editing commands 'sed', ] as const; // Helper function to check if a command is allowed with its options export function isAllowedCommand(command: string): boolean { // Extraer el comando base const baseCommand = command.split(' ')[0]; // Caso especial para sed if (baseCommand === 'sed') { // Verificar que comience con sed -i if (!command.startsWith('sed -i')) { return false; } // Extraer el path del archivo objetivo (último argumento) const matches = command.match(/.*\s+(\/[^\s]+)$/); if (!matches || !matches[1]) { return false; } const filePath = matches[1].replace(/['"]$/, ''); // Eliminar comillas al final si existen // Verificar que el archivo objetivo esté dentro del directorio permitido if (!isWithinAllowedDirectory(filePath)) { return false; } return true; } // Casos especiales para npm create y yarn create if (command.startsWith('npm create') || command.startsWith('yarn create')) { return true; } // Para otros comandos, mantener la lógica existente const commandStart = command.split(' ').slice(0, 2).join(' '); return ALLOWED_COMMANDS.some(cmd => { if (command.startsWith(cmd)) return true; if (commandStart === cmd) return true; return false; }); } // Helper function to check if a path is within allowed directory export function isWithinAllowedDirectory(targetPath: string): boolean { const currentDir = process.cwd(); const absolutePath = path.isAbsolute(targetPath) ? path.resolve(targetPath) : path.resolve(currentDir, targetPath); return absolutePath.startsWith(ALLOWED_DIRECTORY); } // Helper function to resolve path considering current directory export function resolvePath(targetPath: string): string { return path.isAbsolute(targetPath) ? path.resolve(targetPath) : path.resolve(process.cwd(), targetPath); } ``` -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema, } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; // Import schemas import { MkdirSchema, CdSchema } from './src/schemas/directory.js'; // Import handlers import { handleMkdir, handleCd } from './src/handlers/directory.js'; // Import utils import { isAllowedCommand, isWithinAllowedDirectory } from './src/utils/constants.js'; import { createErrorResponse, execAsync, createToolResponse } from './src/utils/command-executor.js'; const ToolInputSchema = ToolSchema.shape.inputSchema; type ToolInput = z.infer<typeof ToolInputSchema>; // Execute command schema const ExecuteCommandSchema = z.object({ command: z.string().refine( (val) => { // Directory commands if (val.startsWith('cd') || val.startsWith('mkdir')) { const parts = val.split(' '); if (parts.length < 2) return false; const dirPath = parts[1]; return isWithinAllowedDirectory(dirPath); } // Check if command is allowed with its options return isAllowedCommand(val); }, 'Command not allowed or path is outside allowed directory' ), }); // Server setup const server = new Server( { name: "terminal-server", version: "0.2.1", }, { capabilities: { tools: {}, }, }, ); // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "execute_command", description: "Execute a terminal command and get its output. Only allowed commands are permitted and must be within the /Users/{username}/the/path/you/use directory.", inputSchema: zodToJsonSchema(ExecuteCommandSchema) as ToolInput, }, { name: "mkdir", description: "Create a new directory within the allowed directory.", inputSchema: zodToJsonSchema(MkdirSchema) as ToolInput, }, { name: "cd", description: "Change to any directory within the allowed directory or its subdirectories.", inputSchema: zodToJsonSchema(CdSchema) as ToolInput, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; let response; switch (name) { case "execute_command": { const parsed = ExecuteCommandSchema.safeParse(args); if (!parsed.success) { throw new Error(`Invalid arguments for execute_command: ${parsed.error}`); } const { command } = parsed.data; const { stdout, stderr } = await execAsync(command); response = createToolResponse(stdout, stderr); break; } case "mkdir": response = await handleMkdir(args); break; case "cd": response = await handleCd(args); break; default: throw new Error(`Unknown tool: ${name}`); } // Convert ToolResponse to the expected format return { _meta: { progressToken: null }, content: response.content }; } catch (error) { const errorResponse = createErrorResponse(error); return { _meta: { progressToken: null }, content: errorResponse.content }; } }); // Start server async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Terminal MCP Server running on localhost"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); }); ```