#
tokens: 3779/50000 10/10 files
lines: off (toggle) GitHub
raw markdown copy
# 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);
});

```