# 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);
});
```