#
tokens: 5345/50000 10/10 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules/
 3 | yarn-debug.log*
 4 | yarn-error.log*
 5 | npm-debug.log*
 6 | 
 7 | # Build outputs
 8 | dist/
 9 | build/
10 | *.js
11 | *.js.map
12 | *.d.ts
13 | 
14 | # Environment variables
15 | .env
16 | .env.local
17 | .env.*.local
18 | 
19 | # IDE
20 | .vscode/
21 | .idea/
22 | *.sublime-project
23 | *.sublime-workspace
24 | 
25 | # OS
26 | .DS_Store
27 | .DS_Store?
28 | ._*
29 | .Spotlight-V100
30 | .Trashes
31 | ehthumbs.db
32 | Thumbs.db
33 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # MCP Terminal Use
 2 | 
 3 | A Model Context Protocol (MCP) server for terminal access. This server allows Claude to interact with specified directories on your system.
 4 | 
 5 | ## Configuration
 6 | 
 7 | ### Environment Variables
 8 | 
 9 | The server can be configured using the following environment variables:
10 | 
11 | - `ALLOWED_DIRECTORY`: The directory path that Claude is allowed to access (default: '${HOME}/your/allowed/directory')
12 | 
13 | You can set these variables either through:
14 | 1. A `.env` file in your project root
15 | 2. Environment variables in your system
16 | 3. Direct configuration in claude_desktop_config.json
17 | 
18 | ### Claude Desktop Configuration
19 | 
20 | Add this to your `claude_desktop_config.json`:
21 | 
22 | ```json
23 | {
24 |   "mcpServers": {
25 |     "terminal": {
26 |       "command": "node",
27 |       "args": [
28 |         "${HOME}/path/to/mcp-terminal-use/dist/index.js"
29 |       ],
30 |       "env": {
31 |         "ALLOWED_DIRECTORY": "${HOME}/your/allowed/directory"
32 |       }
33 |     }
34 |   }
35 | }
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2020",
 4 |     "module": "NodeNext",
 5 |     "moduleResolution": "NodeNext",
 6 |     "esModuleInterop": true,
 7 |     "forceConsistentCasingInFileNames": true,
 8 |     "strict": true,
 9 |     "skipLibCheck": true,
10 |     "outDir": "./dist",
11 |     "rootDir": ".",
12 |     "declaration": true,
13 |     "resolveJsonModule": true
14 |   },
15 |   "include": [
16 |     "./**/*.ts"
17 |   ],
18 |   "exclude": [
19 |     "node_modules",
20 |     "dist"
21 |   ]
22 | }
23 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "terminal",
 3 |   "version": "0.5.1",
 4 |   "description": "MCP server for terminal access zh",
 5 |   "license": "MIT",
 6 |   "author": "Alex Man",
 7 |   "homepage": "",
 8 |   "bugs": "",
 9 |   "type": "module",
10 |   "bin": {
11 |     "mcp-terminal": "dist/index.js"
12 |   },
13 |   "files": [
14 |     "dist"
15 |   ],
16 |   "scripts": {
17 |     "build": "tsc && shx chmod +x dist/*.js",
18 |     "prepare": "npm run build",
19 |     "watch": "tsc --watch"
20 |   },
21 |   "dependencies": {
22 |     "@modelcontextprotocol/sdk": "0.5.0",
23 |     "dotenv": "^16.4.7",
24 |     "glob": "^10.3.10",
25 |     "replicate": "^0.27.1",
26 |     "zod-to-json-schema": "^3.23.5"
27 |   },
28 |   "devDependencies": {
29 |     "@types/node": "^20.11.0",
30 |     "shx": "^0.3.4",
31 |     "typescript": "^5.3.3"
32 |   }
33 | }
34 | 
```

--------------------------------------------------------------------------------
/src/schemas/directory.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | import { isWithinAllowedDirectory, resolvePath, ALLOWED_DIRECTORY } from '../utils/constants.js';
 3 | 
 4 | export const MkdirSchema = z.object({
 5 |   path: z.string().refine(
 6 |     (val) => isWithinAllowedDirectory(val),
 7 |     'Directory must be created within allowed directory'
 8 |   ),
 9 | });
10 | 
11 | export const CdSchema = z.object({
12 |   path: z.string().refine(
13 |     (val) => {
14 |       const targetPath = resolvePath(val);
15 |       return targetPath.startsWith(ALLOWED_DIRECTORY);
16 |     },
17 |     'Can only change to directories within allowed directory'
18 |   ),
19 | });
20 | 
21 | // Types derived from schemas
22 | export type MkdirArgs = z.infer<typeof MkdirSchema>;
23 | export type CdArgs = z.infer<typeof CdSchema>;
24 | 
25 | // Validation functions
26 | export function validateMkdirArgs(args: unknown) {
27 |   return MkdirSchema.safeParse(args);
28 | }
29 | 
30 | export function validateCdArgs(args: unknown) {
31 |   return CdSchema.safeParse(args);
32 | }
33 | 
```

--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from 'zod';
 2 | 
 3 | // Base types for tool responses
 4 | export interface ToolMeta {
 5 |   progressToken: null;
 6 | }
 7 | 
 8 | export interface ToolContent {
 9 |   type: string;
10 |   text: string;
11 | }
12 | 
13 | export interface ToolResponse {
14 |   meta: ToolMeta;
15 |   content: ToolContent[];
16 |   isError?: boolean;
17 | }
18 | 
19 | // Generic handler type
20 | export type ToolHandler = (args: unknown) => Promise<ToolResponse>;
21 | 
22 | // Schema validation result type
23 | export type ValidationResult<T> = {
24 |   success: true;
25 |   data: T;
26 | } | {
27 |   success: false;
28 |   error: z.ZodError;
29 | };
30 | 
31 | // Package manager types
32 | export interface PackageManagerOptions {
33 |   packages?: string[];
34 |   flags?: string[];
35 |   dev?: boolean;
36 | }
37 | 
38 | // Testing types
39 | export interface TestOptions {
40 |   testPath?: string;
41 |   watch?: boolean;
42 |   mode?: 'run' | 'open';
43 |   spec?: string;
44 | }
45 | 
46 | // Linting types
47 | export interface LintOptions {
48 |   path?: string;
49 |   fix?: boolean;
50 |   write?: boolean;
51 |   project?: string;
52 | }
53 | 
54 | // Directory types
55 | export interface DirectoryOptions {
56 |   path: string;
57 | }
58 | 
59 | // Git types
60 | export interface GitOptions {
61 |   path?: string;
62 |   message?: string;
63 |   patch?: string;
64 |   staged?: boolean;
65 | }
66 | 
```

--------------------------------------------------------------------------------
/src/handlers/directory.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { ToolResponse } from '../types/index.js';
 2 | import { validateMkdirArgs, validateCdArgs } from '../schemas/directory.js';
 3 | import { createToolResponse, createErrorResponse, execAsync } from '../utils/command-executor.js';
 4 | 
 5 | export async function handleMkdir(args: unknown): Promise<ToolResponse> {
 6 |   try {
 7 |     const parsed = validateMkdirArgs(args);
 8 |     if (!parsed.success) {
 9 |       throw new Error(`Invalid arguments for mkdir: ${parsed.error}`);
10 |     }
11 | 
12 |     const { path: dirPath } = parsed.data;
13 |     const { stdout, stderr } = await execAsync(`mkdir -p ${dirPath}`);
14 |     
15 |     return createToolResponse(stdout || "Directory created successfully", stderr);
16 |   } catch (error) {
17 |     return createErrorResponse(error);
18 |   }
19 | }
20 | 
21 | export async function handleCd(args: unknown): Promise<ToolResponse> {
22 |   try {
23 |     const parsed = validateCdArgs(args);
24 |     if (!parsed.success) {
25 |       throw new Error(`Invalid arguments for cd: ${parsed.error}`);
26 |     }
27 | 
28 |     const { path: dirPath } = parsed.data;
29 |     try {
30 |       process.chdir(dirPath);
31 |       return createToolResponse(`Changed directory to: ${process.cwd()}`);
32 |     } catch (err) {
33 |       const error = err as Error;
34 |       throw new Error(`Failed to change directory: ${error.message}`);
35 |     }
36 |   } catch (error) {
37 |     return createErrorResponse(error);
38 |   }
39 | }
40 | 
```

--------------------------------------------------------------------------------
/src/utils/command-executor.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { exec, spawn } from 'child_process';
 2 | import { promisify } from 'util';
 3 | 
 4 | // Promisify exec for async/await usage
 5 | export const execAsync = promisify(exec);
 6 | 
 7 | // Types for command execution results
 8 | export interface CommandResult {
 9 |   stdout: string;
10 |   stderr: string;
11 | }
12 | 
13 | export interface ToolResponse {
14 |   meta: {
15 |     progressToken: null;
16 |   };
17 |   content: Array<{
18 |     type: string;
19 |     text: string;
20 |   }>;
21 |   isError?: boolean;
22 | }
23 | 
24 | // Helper function to create a standard tool response
25 | export function createToolResponse(stdout: string, stderr?: string): ToolResponse {
26 |   return {
27 |     meta: {
28 |       progressToken: null,
29 |     },
30 |     content: [
31 |       { type: "text", text: stdout },
32 |       ...(stderr ? [{ type: "text", text: `Error: ${stderr}` }] : []),
33 |     ],
34 |   };
35 | }
36 | 
37 | // Helper function to create an error response
38 | export function createErrorResponse(error: unknown): ToolResponse {
39 |   const errorMessage = error instanceof Error ? error.message : String(error);
40 |   return {
41 |     meta: {
42 |       progressToken: null,
43 |     },
44 |     content: [{ type: "text", text: `Error: ${errorMessage}` }],
45 |     isError: true,
46 |   };
47 | }
48 | 
49 | // Helper function to handle git apply with patch data
50 | export async function gitApplyWithPatch(patch: string): Promise<CommandResult> {
51 |   return new Promise((resolve, reject) => {
52 |     const git = spawn('git', ['apply']);
53 |     let stdout = '';
54 |     let stderr = '';
55 | 
56 |     git.stdout.on('data', (data) => {
57 |       stdout += data.toString();
58 |     });
59 | 
60 |     git.stderr.on('data', (data) => {
61 |       stderr += data.toString();
62 |     });
63 | 
64 |     git.on('close', (code) => {
65 |       if (code === 0) {
66 |         resolve({ stdout, stderr });
67 |       } else {
68 |         reject(new Error(`git apply failed with code ${code}\n${stderr}`));
69 |       }
70 |     });
71 | 
72 |     git.stdin.write(patch);
73 |     git.stdin.end();
74 |   });
75 | }
76 | 
77 | // Helper function to execute a command and return a tool response
78 | export async function executeCommand(command: string): Promise<ToolResponse> {
79 |   try {
80 |     const { stdout, stderr } = await execAsync(command);
81 |     return createToolResponse(stdout, stderr);
82 |   } catch (error) {
83 |     return createErrorResponse(error);
84 |   }
85 | }
86 | 
```

--------------------------------------------------------------------------------
/src/utils/constants.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import path from 'path';
  2 | import dotenv from 'dotenv';
  3 | dotenv.config();
  4 | 
  5 | // Allowed directory for command execution
  6 | export const ALLOWED_DIRECTORY = process.env.ALLOWED_DIRECTORY as string
  7 | 
  8 | // List of allowed commands
  9 | export const ALLOWED_COMMANDS = [
 10 |   // Git commands
 11 |   'git diff',
 12 |   'git diff --staged',
 13 |   'git apply',
 14 |   'git add -p',
 15 |   'git init',
 16 |   'git add',
 17 |   'git commit',
 18 |   'git status',
 19 |   'git log',
 20 |   // Directory commands
 21 |   'mkdir',
 22 |   'cd',
 23 |   // NPM commands
 24 |   'npm init',
 25 |   'npm init -y',
 26 |   'npm install',
 27 |   'npm run',
 28 |   'npm add',
 29 |   'npm remove',
 30 |   'npm create',
 31 |   // Yarn commands
 32 |   'yarn init',
 33 |   'yarn init -y',
 34 |   'yarn install',
 35 |   'yarn run',
 36 |   'yarn add',
 37 |   'yarn remove',
 38 |   'yarn create',
 39 |   // Testing commands
 40 |   'jest',
 41 |   'vitest',
 42 |   'cypress',
 43 |   // Linting and formatting
 44 |   'eslint',
 45 |   'prettier',
 46 |   'tsc',
 47 |   // File editing commands
 48 |   'sed',
 49 | ] as const;
 50 | 
 51 | // Helper function to check if a command is allowed with its options
 52 | export function isAllowedCommand(command: string): boolean {
 53 |   // Extraer el comando base
 54 |   const baseCommand = command.split(' ')[0];
 55 |   
 56 |   // Caso especial para sed
 57 |   if (baseCommand === 'sed') {
 58 |     // Verificar que comience con sed -i
 59 |     if (!command.startsWith('sed -i')) {
 60 |       return false;
 61 |     }
 62 |     
 63 |     // Extraer el path del archivo objetivo (último argumento)
 64 |     const matches = command.match(/.*\s+(\/[^\s]+)$/);
 65 |     if (!matches || !matches[1]) {
 66 |       return false;
 67 |     }
 68 |     
 69 |     const filePath = matches[1].replace(/['"]$/, ''); // Eliminar comillas al final si existen
 70 |     
 71 |     // Verificar que el archivo objetivo esté dentro del directorio permitido
 72 |     if (!isWithinAllowedDirectory(filePath)) {
 73 |       return false;
 74 |     }
 75 |     
 76 |     return true;
 77 |   }
 78 |   
 79 |   // Casos especiales para npm create y yarn create
 80 |   if (command.startsWith('npm create') || command.startsWith('yarn create')) {
 81 |     return true;
 82 |   }
 83 |   
 84 |   // Para otros comandos, mantener la lógica existente
 85 |   const commandStart = command.split(' ').slice(0, 2).join(' ');
 86 |   return ALLOWED_COMMANDS.some(cmd => {
 87 |     if (command.startsWith(cmd)) return true;
 88 |     if (commandStart === cmd) return true;
 89 |     return false;
 90 |   });
 91 | }
 92 | 
 93 | // Helper function to check if a path is within allowed directory
 94 | export function isWithinAllowedDirectory(targetPath: string): boolean {
 95 |   const currentDir = process.cwd();
 96 |   const absolutePath = path.isAbsolute(targetPath) 
 97 |     ? path.resolve(targetPath)
 98 |     : path.resolve(currentDir, targetPath);
 99 |   return absolutePath.startsWith(ALLOWED_DIRECTORY);
100 | }
101 | 
102 | // Helper function to resolve path considering current directory
103 | export function resolvePath(targetPath: string): string {
104 |   return path.isAbsolute(targetPath)
105 |     ? path.resolve(targetPath)
106 |     : path.resolve(process.cwd(), targetPath);
107 | }
108 | 
```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import {
  6 |   CallToolRequestSchema,
  7 |   ListToolsRequestSchema,
  8 |   ToolSchema,
  9 | } from "@modelcontextprotocol/sdk/types.js";
 10 | import { z } from "zod";
 11 | import { zodToJsonSchema } from "zod-to-json-schema";
 12 | 
 13 | // Import schemas
 14 | import { MkdirSchema, CdSchema } from './src/schemas/directory.js';
 15 | 
 16 | // Import handlers
 17 | import { handleMkdir, handleCd } from './src/handlers/directory.js';
 18 | 
 19 | // Import utils
 20 | import { isAllowedCommand, isWithinAllowedDirectory } from './src/utils/constants.js';
 21 | import { createErrorResponse, execAsync, createToolResponse } from './src/utils/command-executor.js';
 22 | 
 23 | const ToolInputSchema = ToolSchema.shape.inputSchema;
 24 | type ToolInput = z.infer<typeof ToolInputSchema>;
 25 | 
 26 | // Execute command schema
 27 | const ExecuteCommandSchema = z.object({
 28 |   command: z.string().refine(
 29 |     (val) => {
 30 |       // Directory commands
 31 |       if (val.startsWith('cd') || val.startsWith('mkdir')) {
 32 |         const parts = val.split(' ');
 33 |         if (parts.length < 2) return false;
 34 |         const dirPath = parts[1];
 35 |         return isWithinAllowedDirectory(dirPath);
 36 |       }
 37 |       
 38 |       // Check if command is allowed with its options
 39 |       return isAllowedCommand(val);
 40 |     },
 41 |     'Command not allowed or path is outside allowed directory'
 42 |   ),
 43 | });
 44 | 
 45 | // Server setup
 46 | const server = new Server(
 47 |   {
 48 |     name: "terminal-server",
 49 |     version: "0.2.1",
 50 |   },
 51 |   {
 52 |     capabilities: {
 53 |       tools: {},
 54 |     },
 55 |   },
 56 | );
 57 | 
 58 | // Tool handlers
 59 | server.setRequestHandler(ListToolsRequestSchema, async () => {
 60 |   return {
 61 |     tools: [
 62 |       {
 63 |         name: "execute_command",
 64 |         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.",
 65 |         inputSchema: zodToJsonSchema(ExecuteCommandSchema) as ToolInput,
 66 |       },
 67 |       {
 68 |         name: "mkdir",
 69 |         description: "Create a new directory within the allowed directory.",
 70 |         inputSchema: zodToJsonSchema(MkdirSchema) as ToolInput,
 71 |       },
 72 |       {
 73 |         name: "cd",
 74 |         description: "Change to any directory within the allowed directory or its subdirectories.",
 75 |         inputSchema: zodToJsonSchema(CdSchema) as ToolInput,
 76 |       },
 77 |     ],
 78 |   };
 79 | });
 80 | 
 81 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
 82 |   try {
 83 |     const { name, arguments: args } = request.params;
 84 | 
 85 |     let response;
 86 |     switch (name) {
 87 |       case "execute_command": {
 88 |         const parsed = ExecuteCommandSchema.safeParse(args);
 89 |         if (!parsed.success) {
 90 |           throw new Error(`Invalid arguments for execute_command: ${parsed.error}`);
 91 |         }
 92 |         
 93 |         const { command } = parsed.data;
 94 |         const { stdout, stderr } = await execAsync(command);
 95 |         response = createToolResponse(stdout, stderr);
 96 |         break;
 97 |       }
 98 | 
 99 |       case "mkdir":
100 |         response = await handleMkdir(args);
101 |         break;
102 | 
103 |       case "cd":
104 |         response = await handleCd(args);
105 |         break;
106 | 
107 |       default:
108 |         throw new Error(`Unknown tool: ${name}`);
109 |     }
110 | 
111 |     // Convert ToolResponse to the expected format
112 |     return {
113 |       _meta: {
114 |         progressToken: null
115 |       },
116 |       content: response.content
117 |     };
118 |   } catch (error) {
119 |     const errorResponse = createErrorResponse(error);
120 |     return {
121 |       _meta: {
122 |         progressToken: null
123 |       },
124 |       content: errorResponse.content
125 |     };
126 |   }
127 | });
128 | 
129 | // Start server
130 | async function runServer() {
131 |   const transport = new StdioServerTransport();
132 |   await server.connect(transport);
133 |   console.error("Terminal MCP Server running on localhost");
134 | }
135 | 
136 | runServer().catch((error) => {
137 |   console.error("Fatal error running server:", error);
138 |   process.exit(1);
139 | });
140 | 
```