#
tokens: 10075/50000 19/19 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .eslintrc.json
├── .github
│   ├── ISSUE_TEMPLATE.md
│   └── workflows
│       ├── ci.yml
│       └── publish.yml
├── .gitignore
├── .npmignore
├── .prettierrc
├── CONTRIBUTING.md
├── examples
│   └── basic-usage.js
├── guide
│   ├── add_mcp.png
│   └── result.png
├── jest.config.js
├── LICENSE
├── package.json
├── README.md
├── src
│   ├── cli.ts
│   ├── index.ts
│   └── utils
│       ├── buffer.test.ts
│       ├── buffer.ts
│       ├── command-runner.test.ts
│       └── command-runner.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
{
  "semi": true,
  "trailingComma": "all",
  "singleQuote": true,
  "printWidth": 100,
  "tabWidth": 2,
  "endOfLine": "auto"
} 
```

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
# Source code
src/

# Tests
*.test.ts
*.spec.ts
test/
tests/
__tests__/
*.test.js
*.spec.js
coverage/

# Development files
.github/
.vscode/
.editorconfig
.eslintrc*
.prettierrc*
tsconfig.json
jest.config.*
*.tsbuildinfo

# Examples
examples/

# Git files
.git/
.gitignore

# CI/CD
.github/

# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Environment variables
.env* 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Dependency directories
node_modules/
pnpm-lock.yaml

# Built output
dist/
build/
out/

# TypeScript cache
*.tsbuildinfo

# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Test coverage
coverage/

# System files
.DS_Store
Thumbs.db 
```

--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------

```json
{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "plugins": ["@typescript-eslint"],
  "env": {
    "node": true,
    "es6": true
  },
  "parserOptions": {
    "ecmaVersion": 2022,
    "sourceType": "module"
  },
  "rules": {
    "@typescript-eslint/explicit-function-return-type": "warn",
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
    "@typescript-eslint/no-explicit-any": "warn",
    "no-console": "off"
  }
} 
```

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

```markdown
# MCP Command Proxy

An MCP (Model Context Protocol) server that acts as a proxy for CLI commands, specifically designed for Expo development but adaptable for any command-line application.

## How to use in Cursor (Expo example)

1. Go to the directory of your Expo project
2. Run `npx mcp-command-proxy --prefix "ExpoServer" --command "expo start" --port 8383`
3. Go to Cursor settings -> MCP -> +Add new MCP server, like this:  
![add_mcp_server](guide/add_mcp.png)
4. Set the name to "ExpoServer", Type to "SSE", URL to `http://localhost:8383/sse`
5. Click "Save" and you should now be able to use the MCP server in Cursor. Like this:
![mcp_server_in_cursor](guide/result.png)

Recommended to use the `--port 8383` flag to avoid conflicts with other servers.
Also, you can add following instruction to .cursorrules file:
```
You can use MCP getRecentLogs tool to get the most recent logs from Expo server. And if needed, you can send key presses to the running process using sendKeyPress tool.
```


## Features

- **Command Proxying**: Run any CLI command through the MCP server
- **Log Collection**: Capture and store logs from running processes (configurable buffer size)
- **Key Press Forwarding**: Forward key presses from client to the running process
- **Transparent Experience**: The end user sees the command output exactly as if they ran it directly
- **Interactive Commands**: Works with interactive CLI tools like Expo
- **MCP Integration**: Built using the MCP SDK for easy integration with Claude and other MCP-enabled AI assistants

## How It Works

1. The server starts a specified command in a pseudo-terminal (PTY)
2. All stdout/stderr output is:
   - Streamed to the client in real-time
   - Stored in a circular buffer (configurable size, default 300 lines)
3. Key presses from the client are forwarded to the running process
4. The server provides tools to:
   - View collected logs
   - Send key presses to the process
   - Get the current state of the process

## Use Cases

- **Expo Development**: Run `expo start` and interact with it while collecting logs
- **Build Processes**: Monitor build processes and analyze logs
- **Long-running Services**: Monitor services and keep recent log history
- **Remote Command Execution**: Execute and monitor commands from remote clients

## Requirements

- Node.js 18+ 
- TypeScript
- pnpm (recommended) or npm

## Installation

```bash
# Install dependencies
pnpm install

# Build the project
pnpm build

# Run directly
pnpm start -- --prefix "MyServer" --command "expo start"

# Or install globally
pnpm install -g
mcp-command-proxy --prefix "MyServer" --command "expo start"
```

## Usage

### Basic Usage

```bash
# Using the CLI
mcp-command-proxy --prefix "ExpoServer" --command "expo start"

# Or programmatically
import { createServer } from 'mcp-command-proxy';

const server = await createServer({
  prefix: 'ExpoServer',
  command: 'expo start',
  bufferSize: 500,
  port: 8080
});

// To stop the server later
server.stop();
```

### Options

- `--prefix, -p`: Name/prefix for the server (default: "CommandProxy")
- `--command, -c`: Command to run (required)
- `--buffer-size, -b`: Number of log lines to keep in memory (default: 300)
- `--port`: Port for HTTP server (default: 8080)
- `--help, -h`: Show help

### MCP Integration

This server implements the following MCP tools:

1. `getRecentLogs`: Returns the most recent logs from the buffer
   - Parameters: 
     - `limit` (optional): Number of logs to return (default: 100)
     - `types` (optional): Types of logs to include (stdout, stderr, system) (default: all)

2. `sendKeyPress`: Sends a key press to the running process
   - Parameters:
     - `key`: Key to send (e.g. "enter", "a", "1", "space")

3. `getProcessStatus`: Returns the current status of the process
   - Parameters: None

## Examples

### Running Expo Start

```bash
mcp-command-proxy -p "ExpoServer" -c "expo start" -b 500
```

### Using with Claude

1. Configure Claude to connect to this MCP server (SSE endpoint: http://localhost:8080/sse)
2. Ask Claude to run Expo or other commands
3. Claude can analyze logs and help troubleshoot issues

## Development

```bash
# Clone the repository
git clone https://github.com/hormold/mcp-command-proxy.git
cd mcp-command-proxy

# Install dependencies
pnpm install

# Build the project
pnpm build

# Run in development mode
pnpm dev
```

## License

MIT 
```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
# Contributing to MCP Command Proxy

Thank you for your interest in contributing to MCP Command Proxy! This document provides guidelines and instructions for contributing.

## Code of Conduct

Please be respectful and considerate of others when contributing to this project.

## How Can I Contribute?

### Reporting Bugs

When reporting bugs, please include:

- A clear and descriptive title
- Steps to reproduce the issue
- Expected behavior
- Actual behavior
- Screenshots if applicable
- Your environment details (OS, Node.js version, etc.)

### Suggesting Enhancements

Enhancement suggestions are welcome! Please provide:

- A clear and descriptive title
- A detailed description of the proposed enhancement
- Any relevant examples or mock-ups

### Pull Requests

1. Fork the repository
2. Create a new branch for your feature or bug fix
3. Write your code, with tests if applicable
4. Ensure all tests pass
5. Submit a pull request with a clear description of the changes

Please make sure your code follows our style guidelines:

- Use TypeScript
- Format code with Prettier
- Follow ESLint rules
- Write meaningful commit messages

## Development Setup

```bash
# Clone your fork of the repo
git clone https://github.com/YOUR_USERNAME/mcp-command-proxy.git

# Navigate to the project directory
cd mcp-command-proxy

# Install dependencies
pnpm install

# Build the project
pnpm build

# Run tests
pnpm test
```

## Project Structure

- `src/` - Source code
  - `index.ts` - Main entry point for library
  - `cli.ts` - CLI entry point
  - `utils/` - Utility functions
    - `command-runner.ts` - Core command running functionality
    - `buffer.ts` - Circular buffer implementation
- `dist/` - Compiled JavaScript code

## License

By contributing, you agree that your contributions will be licensed under the project's [MIT License](LICENSE). 
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------

```markdown
## Description
<!-- A clear and concise description of the issue -->

## Steps to Reproduce
<!-- Steps to reproduce the behavior -->
1. 
2. 
3. 

## Expected Behavior
<!-- What you expected to happen -->

## Actual Behavior
<!-- What actually happened -->

## Environment
- OS: <!-- e.g. macOS, Windows, Linux -->
- Node.js version: <!-- e.g. 18.0.0 -->
- MCP Command Proxy version: <!-- e.g. 1.0.0 -->

## Additional Context
<!-- Any other context, screenshots, or relevant information about the issue --> 
```

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

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "dist",
    "sourceMap": true,
    "declaration": true,
    "resolveJsonModule": true,
    "rootDir": "src",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
} 
```

--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------

```javascript
/** @type {import('ts-jest').JestConfigWithTsJest} */
export default {
  preset: 'ts-jest/presets/default-esm',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/*.test.ts'],
  transform: {
    '^.+\\.tsx?$': [
      'ts-jest',
      {
        useESM: true,
      },
    ],
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '^(\\.{1,2}/.*)\\.js$': '$1',
  },
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.test.{ts,tsx}',
  ],
  extensionsToTreatAsEsm: ['.ts', '.tsx'],
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
}; 
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18.x, 20.x]

    steps:
    - uses: actions/checkout@v3
    
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        
    - name: Setup PNPM
      uses: pnpm/action-setup@v2
      with:
        version: 8
        
    - name: Install dependencies
      run: pnpm install
      
    - name: Run linter
      run: pnpm lint
      
    - name: Run tests
      run: pnpm test
      
    - name: Build
      run: pnpm build 
```

--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------

```yaml
name: Publish to NPM

on:
  release:
    types: [created]

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20.x'
          registry-url: 'https://registry.npmjs.org/'
          
      - name: Setup PNPM
        uses: pnpm/action-setup@v2
        with:
          version: 8
          
      - name: Install dependencies
        run: pnpm install
        
      - name: Run tests
        run: pnpm test
        
      - name: Build
        run: pnpm build
        
      - name: Publish to NPM
        run: pnpm publish --no-git-checks
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
          PNPM_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 
```

--------------------------------------------------------------------------------
/examples/basic-usage.js:
--------------------------------------------------------------------------------

```javascript
/**
 * Basic usage example for mcp-command-proxy
 * 
 * This example shows how to create an MCP server that runs the "echo" command
 * and demonstrates how to interact with it programmatically.
 * 
 * @copyright 2025 Hormold
 * @license MIT
 */

import { createServer } from 'mcp-command-proxy';

// Create a server that runs "echo Hello, MCP!"
const server = await createServer({
  prefix: 'ExampleServer',
  command: 'echo "Hello, MCP!"',
  bufferSize: 100,
  port: 8080
});

console.log('MCP server started. Press Ctrl+C to exit.');

// Handle exit signals
process.on('SIGINT', () => {
  console.log('Shutting down MCP server...');
  server.stop();
  process.exit(0);
});

/**
 * In a real application, you might want to do something with the server
 * For example, you could expose it via a REST API or use it for automation
 * 
 * Example of accessing logs programmatically:
 * 
 * import { CommandRunner } from 'mcp-command-proxy';
 * 
 * // Create a command runner directly
 * const runner = new CommandRunner({
 *   command: 'npm start',
 *   logBufferSize: 500
 * });
 * 
 * // Handle logs
 * runner.on('log', (entry) => {
 *   console.log(`[${entry.type}] ${entry.content}`);
 * });
 * 
 * // Start the command
 * runner.start();
 * 
 * // Send input
 * runner.write('y\n');
 * 
 * // Get logs
 * const logs = runner.getLogs();
 * 
 * // Stop the command
 * runner.stop();
 */ 
```

--------------------------------------------------------------------------------
/src/utils/buffer.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * A circular buffer implementation for storing log lines with a fixed capacity
 */
export class CircularBuffer<T> {
  private buffer: T[];
  private head: number = 0;
  private tail: number = 0;
  private size: number = 0;
  private readonly capacity: number;

  /**
   * Create a new circular buffer
   * @param capacity Maximum number of items the buffer can hold
   */
  constructor(capacity: number) {
    this.buffer = new Array<T>(capacity);
    this.capacity = capacity;
  }

  /**
   * Add an item to the buffer, overwriting the oldest item if full
   * @param item Item to add to the buffer
   */
  push(item: T): void {
    this.buffer[this.head] = item;
    this.head = (this.head + 1) % this.capacity;

    if (this.size < this.capacity) {
      this.size++;
    } else {
      // Buffer is full, move tail forward
      this.tail = (this.tail + 1) % this.capacity;
    }
  }

  /**
   * Get all items currently in the buffer in order of insertion
   * @returns Array of items in order of insertion (oldest to newest)
   */
  getAll(): T[] {
    const result: T[] = [];
    let current = this.tail;

    for (let i = 0; i < this.size; i++) {
      result.push(this.buffer[current]);
      current = (current + 1) % this.capacity;
    }

    return result;
  }

  /**
   * Get the number of items in the buffer
   */
  getSize(): number {
    return this.size;
  }

  /**
   * Get the capacity of the buffer
   */
  getCapacity(): number {
    return this.capacity;
  }

  /**
   * Clear all items from the buffer
   */
  clear(): void {
    this.head = 0;
    this.tail = 0;
    this.size = 0;
  }
} 
```

--------------------------------------------------------------------------------
/src/utils/buffer.test.ts:
--------------------------------------------------------------------------------

```typescript
import { CircularBuffer } from './buffer.js';

describe('CircularBuffer', () => {
  describe('constructor', () => {
    it('should create a buffer with the specified capacity', () => {
      const buffer = new CircularBuffer<number>(5);
      expect(buffer.getCapacity()).toBe(5);
      expect(buffer.getSize()).toBe(0);
    });
  });

  describe('push', () => {
    it('should add items to the buffer', () => {
      const buffer = new CircularBuffer<number>(3);
      buffer.push(1);
      buffer.push(2);
      expect(buffer.getSize()).toBe(2);
      expect(buffer.getAll()).toEqual([1, 2]);
    });

    it('should overwrite old items when full', () => {
      const buffer = new CircularBuffer<number>(3);
      buffer.push(1);
      buffer.push(2);
      buffer.push(3);
      buffer.push(4);
      expect(buffer.getSize()).toBe(3);
      expect(buffer.getAll()).toEqual([2, 3, 4]);
    });
  });

  describe('getAll', () => {
    it('should return all items in order', () => {
      const buffer = new CircularBuffer<string>(5);
      buffer.push('a');
      buffer.push('b');
      buffer.push('c');
      expect(buffer.getAll()).toEqual(['a', 'b', 'c']);
    });

    it('should return an empty array for an empty buffer', () => {
      const buffer = new CircularBuffer<string>(5);
      expect(buffer.getAll()).toEqual([]);
    });
  });

  describe('clear', () => {
    it('should remove all items from the buffer', () => {
      const buffer = new CircularBuffer<number>(5);
      buffer.push(1);
      buffer.push(2);
      buffer.push(3);
      buffer.clear();
      expect(buffer.getSize()).toBe(0);
      expect(buffer.getAll()).toEqual([]);
    });
  });
}); 
```

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

```json
{
  "name": "mcp-command-proxy",
  "version": "1.0.2",
  "description": "MCP server that proxies CLI commands and collects logs",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "mcp-command-proxy": "dist/cli.js"
  },
  "scripts": {
    "build": "tsc",
    "start": "node dist/cli.js",
    "dev": "ts-node --esm src/cli.ts",
    "test": "jest",
    "lint": "eslint . --ext .ts",
    "format": "prettier --write \"src/**/*.ts\"",
    "prepublishOnly": "npm run build",
    "prepare": "npm run build"
  },
  "keywords": [
    "mcp",
    "cli",
    "proxy",
    "logs",
    "expo",
    "model-context-protocol"
  ],
  "author": "Hormold",
  "repository": {
    "type": "git",
    "url": "https://github.com/hormold/mcp-command-proxy.git"
  },
  "bugs": {
    "url": "https://github.com/hormold/mcp-command-proxy/issues"
  },
  "homepage": "https://github.com/hormold/mcp-command-proxy#readme",
  "license": "MIT",
  "devDependencies": {
    "@types/express": "^5.0.0",
    "@types/jest": "^29.5.11",
    "@types/node": "^20.10.4",
    "@typescript-eslint/eslint-plugin": "^6.13.2",
    "@typescript-eslint/parser": "^6.13.2",
    "eslint": "^8.55.0",
    "jest": "^29.7.0",
    "prettier": "^3.1.0",
    "ts-jest": "^29.1.1",
    "ts-node": "^10.9.1",
    "typescript": "^5.3.3"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.6.1",
    "express": "^4.21.2",
    "node-pty": "^1.0.0",
    "zod": "^3.24.2"
  },
  "mcp": {
    "name": "Command Proxy",
    "description": "MCP server that proxies CLI commands and collects logs",
    "transportType": "sse",
    "sse": {
      "endpoint": "/sse",
      "messagesEndpoint": "/messages"
    },
    "capabilities": {
      "resources": true,
      "tools": true
    },
    "tools": [
      {
        "name": "getRecentLogs",
        "description": "Get the most recent logs from the running command",
        "parameters": {
          "limit": "number?",
          "types": "string[]?"
        }
      },
      {
        "name": "sendKeyPress",
        "description": "Send a key press to the running command",
        "parameters": {
          "key": "string"
        }
      },
      {
        "name": "getProcessStatus",
        "description": "Get the current status of the running command",
        "parameters": {}
      }
    ],
    "resources": [
      {
        "name": "server-info",
        "description": "Information about the server and available tools",
        "uri": "server://info"
      }
    ]
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "files": [
    "dist",
    "README.md",
    "LICENSE"
  ]
}
```

--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

import { createServer } from './index.js';

// Parse command line arguments
export function parseArgs(): { prefix: string; command: string; bufferSize: number; port: number } {
  const args = process.argv.slice(2);
  let prefix = 'CommandProxy';
  let command = '';
  let bufferSize = 300;
  let port = 8080;

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    
    if (arg === '--prefix' || arg === '-p') {
      prefix = args[++i] || prefix;
    } else if (arg === '--command' || arg === '-c') {
      command = args[++i] || command;
    } else if (arg === '--buffer-size' || arg === '-b') {
      bufferSize = parseInt(args[++i] || String(bufferSize), 10);
    } else if (arg === '--port') {
      port = parseInt(args[++i] || String(port), 10);
    } else if (arg === '--help' || arg === '-h') {
      showHelp();
      process.exit(0);
    }
  }

  if (!command) {
    console.error('Error: Command is required');
    showHelp();
    process.exit(1);
  }

  return { prefix, command, bufferSize, port };
}

export function showHelp(): void {
  console.log(`
MCP Command Proxy - Run CLI commands with MCP

Usage:
  mcp-command-proxy [options]

Options:
  --prefix, -p        Name/prefix for the server (default: "CommandProxy")
  --command, -c       Command to run (required)
  --buffer-size, -b   Number of log lines to keep in memory (default: 300)
  --port              Port for HTTP server (default: 8080)
  --help, -h          Show this help message

Example:
  mcp-command-proxy -p "ExpoServer" -c "expo start" -b 500 --port 8080
  `);
}

// Main function
export async function main(): Promise<void> {
  try {
    const { prefix, command, bufferSize, port } = parseArgs();
    
    console.log(`Starting MCP Command Proxy with:
  - Prefix: ${prefix}
  - Command: ${command}
  - Buffer Size: ${bufferSize}
  - Port: ${port}
`);

    const server = await createServer({
      prefix,
      command,
      bufferSize,
      port
    });

    // Handle exit signals
    const exitHandler = (): void => {
      console.log('\nShutting down MCP Command Proxy...');
      server.stop();
      process.exit(0);
    };

    process.on('SIGINT', exitHandler);
    process.on('SIGTERM', exitHandler);
    
    console.log(`
MCP Command Proxy is running!
- SSE endpoint: http://localhost:${port}/sse
- Messages endpoint: http://localhost:${port}/messages

Connect your MCP client to these endpoints.
`);
  } catch (error) {
    console.error('Error starting MCP Command Proxy:', error);
    process.exit(1);
  }
}

// Only run main if this is the entry point
main().catch(error => {
  console.error('Unhandled error:', error);
  process.exit(1);
}); 
```

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

```typescript
import * as pty from 'node-pty';
import { EventEmitter } from 'events';
import { CircularBuffer } from './buffer.js';

/**
 * Log entry type for storing command output
 */
export interface LogEntry {
  timestamp: number;
  content: string;
  type: 'stdout' | 'stderr' | 'system';
}

/**
 * Process status type
 */
export enum ProcessStatus {
  RUNNING = 'running',
  STOPPED = 'stopped',
  ERROR = 'error',
}

/**
 * Events emitted by the CommandRunner
 */
export interface CommandRunnerEvents {
  log: (entry: LogEntry) => void;
  exit: (code: number, signal?: string) => void;
  error: (error: Error) => void;
  statusChange: (status: ProcessStatus) => void;
}

/**
 * CommandRunner options
 */
export interface CommandRunnerOptions {
  command: string;
  args?: string[];
  cwd?: string;
  env?: NodeJS.ProcessEnv;
  logBufferSize?: number;
}

/**
 * Class that runs a command in a pseudo-terminal and captures output
 */
export class CommandRunner extends EventEmitter {
  private process: pty.IPty | null = null;
  private logBuffer: CircularBuffer<LogEntry>;
  private status: ProcessStatus = ProcessStatus.STOPPED;
  private readonly command: string;
  private readonly args: string[];
  private readonly cwd: string;
  private readonly env: NodeJS.ProcessEnv;

  /**
   * Create a new CommandRunner
   */
  constructor(options: CommandRunnerOptions) {
    super();
    
    // Parse command and arguments
    const parts = options.command.split(' ');
    this.command = parts[0];
    this.args = parts.slice(1).concat(options.args || []);
    
    this.cwd = options.cwd || process.cwd();
    this.env = options.env || process.env;
    this.logBuffer = new CircularBuffer<LogEntry>(options.logBufferSize || 300);
  }

  /**
   * Start the command process
   */
  start(): void {
    try {
      // Add a system log entry
      this.addLogEntry(`Starting command: ${this.command} ${this.args.join(' ')}`, 'system');
      
      // Spawn the process
      this.process = pty.spawn(this.command, this.args, {
        name: 'xterm-color',
        cols: 80,
        rows: 30,
        cwd: this.cwd,
        env: { ...this.env, FORCE_COLOR: '1', TERM: 'xterm-256color' },
        handleFlowControl: true,
      });

      // Set status to running
      this.setStatus(ProcessStatus.RUNNING);

      // Handle data events (output)
      this.process.onData((data) => {
        this.addLogEntry(data, 'stdout');
      });

      // Handle exit events
      this.process.onExit(({ exitCode, signal }) => {
        this.addLogEntry(`Process exited with code ${exitCode} and signal ${signal || 'none'}`, 'system');
        this.setStatus(ProcessStatus.STOPPED);
        this.emit('exit', exitCode, signal);
        this.process = null;
      });
    } catch (error) {
      this.setStatus(ProcessStatus.ERROR);
      this.addLogEntry(`Error starting command: ${error}`, 'system');
      this.emit('error', error);
    }
  }

  /**
   * Stop the command process
   */
  stop(): void {
    if (this.process && this.status === ProcessStatus.RUNNING) {
      this.addLogEntry('Stopping command...', 'system');
      this.process.kill();
    }
  }

  /**
   * Send data (key presses) to the process
   */
  write(data: string): void {
    if (this.process && this.status === ProcessStatus.RUNNING) {
      this.addLogEntry(`Attempting to write key: ${JSON.stringify(data)}`, 'system');
      try {
        this.process.write(data);
        this.addLogEntry(`Successfully wrote key`, 'system');
      } catch (err) {
        this.addLogEntry(`Failed to write key: ${err}`, 'system');
      }
    } else {
      this.addLogEntry('Cannot write to process: not running', 'system');
    }
  }

  /**
   * Get all log entries
   */
  getLogs(): LogEntry[] {
    return this.logBuffer.getAll();
  }

  /**
   * Get current process status
   */
  getStatus(): ProcessStatus {
    return this.status;
  }

  /**
   * Add a log entry to the buffer and emit a log event
   */
  private addLogEntry(content: string, type: LogEntry['type']): void {
    const entry: LogEntry = {
      timestamp: Date.now(),
      content,
      type,
    };
    
    this.logBuffer.push(entry);
    this.emit('log', entry);
  }

  /**
   * Set the process status and emit a status change event
   */
  private setStatus(status: ProcessStatus): void {
    this.status = status;
    this.emit('statusChange', this.status);
  }
} 
```

--------------------------------------------------------------------------------
/src/utils/command-runner.test.ts:
--------------------------------------------------------------------------------

```typescript
import { CommandRunner, ProcessStatus } from './command-runner';
import * as pty from 'node-pty';

// Mock node-pty
jest.mock('node-pty', () => ({
  spawn: jest.fn(() => ({
    onData: jest.fn(),
    onExit: jest.fn(),
    write: jest.fn(),
    kill: jest.fn(),
  })),
}));

describe('CommandRunner', () => {
  let runner: CommandRunner;
  let mockProcess: jest.Mocked<pty.IPty>;

  beforeEach(() => {
    jest.clearAllMocks();
    mockProcess = {
      onData: jest.fn(),
      onExit: jest.fn(),
      write: jest.fn(),
      kill: jest.fn(),
    } as unknown as jest.Mocked<pty.IPty>;
    (pty.spawn as jest.Mock).mockReturnValue(mockProcess);
  });

  describe('constructor', () => {
    it('should initialize with default options', () => {
      runner = new CommandRunner({ command: 'test-cmd' });
      expect(runner.getStatus()).toBe(ProcessStatus.STOPPED);
      expect(runner.getLogs()).toEqual([]);
    });

    it('should parse command and arguments correctly', () => {
      runner = new CommandRunner({ 
        command: 'test-cmd arg1 arg2',
        args: ['arg3']
      });
      runner.start();
      expect(pty.spawn).toHaveBeenCalledWith(
        'test-cmd',
        ['arg1', 'arg2', 'arg3'],
        expect.any(Object)
      );
    });

    it('should use provided options', () => {
      const cwd = '/test/dir';
      const env = { TEST_ENV: 'value' };
      runner = new CommandRunner({ 
        command: 'test-cmd',
        cwd,
        env,
        logBufferSize: 100
      });
      runner.start();
      expect(pty.spawn).toHaveBeenCalledWith(
        'test-cmd',
        [],
        expect.objectContaining({
          cwd,
          env: expect.objectContaining(env)
        })
      );
    });
  });

  describe('process lifecycle', () => {
    beforeEach(() => {
      runner = new CommandRunner({ command: 'test-cmd' });
    });

    it('should start process and update status', () => {
      const statusListener = jest.fn();
      runner.on('statusChange', statusListener);
      
      runner.start();
      
      expect(runner.getStatus()).toBe(ProcessStatus.RUNNING);
      expect(statusListener).toHaveBeenCalledWith(ProcessStatus.RUNNING);
      expect(pty.spawn).toHaveBeenCalled();
    });

    it('should handle process exit', () => {
      const exitListener = jest.fn();
      const statusListener = jest.fn();
      runner.on('exit', exitListener);
      runner.on('statusChange', statusListener);
      
      runner.start();
      const exitCallback = (mockProcess.onExit as jest.Mock).mock.calls[0][0];
      exitCallback({ exitCode: 0, signal: null });
      
      expect(runner.getStatus()).toBe(ProcessStatus.STOPPED);
      expect(exitListener).toHaveBeenCalledWith(0, null);
      expect(statusListener).toHaveBeenCalledWith(ProcessStatus.STOPPED);
    });

    it('should stop process', () => {
      runner.start();
      runner.stop();
      
      expect(mockProcess.kill).toHaveBeenCalled();
    });
  });

  describe('log management', () => {
    beforeEach(() => {
      runner = new CommandRunner({ command: 'test-cmd' });
    });

    it('should capture stdout', () => {
      const logListener = jest.fn();
      runner.on('log', logListener);
      
      runner.start();
      const dataCallback = (mockProcess.onData as jest.Mock).mock.calls[0][0];
      dataCallback('test output');
      
      const logs = runner.getLogs();
      expect(logs).toHaveLength(2); // Including start command log
      expect(logs[1].content).toBe('test output');
      expect(logs[1].type).toBe('stdout');
      expect(logListener).toHaveBeenCalledWith(expect.objectContaining({
        content: 'test output',
        type: 'stdout'
      }));
    });

    it('should respect log buffer size', () => {
      runner = new CommandRunner({ 
        command: 'test-cmd',
        logBufferSize: 2
      });
      
      runner.start();
      const dataCallback = (mockProcess.onData as jest.Mock).mock.calls[0][0];
      dataCallback('output1');
      dataCallback('output2');
      dataCallback('output3');
      
      const logs = runner.getLogs();
      expect(logs).toHaveLength(2);
      expect(logs.map(l => l.content)).toEqual(['output2', 'output3']);
    });
  });

  describe('error handling', () => {
    it('should handle spawn errors', () => {
      const error = new Error('Spawn error');
      (pty.spawn as jest.Mock).mockImplementation(() => {
        throw error;
      });

      const errorListener = jest.fn();
      runner = new CommandRunner({ command: 'test-cmd' });
      runner.on('error', errorListener);
      
      runner.start();
      
      expect(runner.getStatus()).toBe(ProcessStatus.ERROR);
      expect(errorListener).toHaveBeenCalledWith(error);
    });

    it('should handle write errors', () => {
      runner = new CommandRunner({ command: 'test-cmd' });
      runner.start();
      
      mockProcess.write.mockImplementation(() => {
        throw new Error('Write error');
      });
      
      runner.write('test');
      
      const logs = runner.getLogs();
      expect(logs.some(log => 
        log.type === 'system' && 
        log.content.includes('Failed to write key')
      )).toBe(true);
    });
  });

  describe('write', () => {
    beforeEach(() => {
      runner = new CommandRunner({ command: 'test-cmd' });
    });

    it('should write to process when running', () => {
      runner.start();
      runner.write('test input');
      
      expect(mockProcess.write).toHaveBeenCalledWith('test input');
    });

    it('should not write when process is stopped', () => {
      runner.write('test input');
      
      expect(mockProcess.write).not.toHaveBeenCalled();
      const logs = runner.getLogs();
      expect(logs.some(log => 
        log.type === 'system' && 
        log.content.includes('Cannot write to process: not running')
      )).toBe(true);
    });
  });
});
```

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

```typescript
/**
 * MCP Command Proxy
 * 
 * A Model Context Protocol (MCP) server for proxying CLI commands and collecting logs
 * 
 * @module mcp-command-proxy
 */

import express from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { z } from 'zod';
import { CommandRunner, ProcessStatus, LogEntry } from './utils/command-runner.js';

/**
 * Create an MCP server for proxying CLI commands
 */
export async function createServer(options: {
  prefix: string;
  command: string;
  bufferSize?: number;
  port: number;
}) {
  const { prefix, command, bufferSize = 300, port } = options;
  
  // Create Express app
  const app = express();
  
  // Parse JSON bodies
  app.use(express.json());
  
  // Create MCP server
  const server = new McpServer({
    name: `${prefix} MCP Server`,
    version: '1.0.0'
  });
  
  // Create command runner
  const commandRunner = new CommandRunner({
    command,
    logBufferSize: bufferSize,
  });
  
  // Setup command runner event handlers
  commandRunner.on('log', (entry: LogEntry) => {
    // Log to console for debugging
    if (entry.type === 'stdout') {
      process.stdout.write(entry.content);
    } else if (entry.type === 'stderr') {
      process.stderr.write(entry.content);
    } else {
      console.log(`[${prefix}] ${entry.content}`);
    }
  });
  
  commandRunner.on('exit', (code: number) => {
    console.log(`[${prefix}] Command exited with code ${code}`);
  });
  
  commandRunner.on('error', (error: Error) => {
    console.error(`[${prefix}] Command error:`, error);
  });
  
  // Add MCP tools
  
  // Add a resource for recent logs
  server.resource(
    'logs',
    'logs://recent',
    async () => {
      const logs = commandRunner.getLogs()
        .slice(-100); // Default to 100 most recent logs
      
      return {
        contents: [{
          uri: 'logs://recent',
          text: JSON.stringify(logs, null, 2)
        }]
      };
    }
  );
  
  // Add tool to get recent logs
  server.tool(
    'getRecentLogs',
    {
      limit: z.number().optional().default(100),
      types: z.array(z.enum(['stdout', 'stderr', 'system'])).optional().default(['stdout', 'stderr', 'system'])
    },
    async ({ limit, types }) => {
      const logs = commandRunner.getLogs()
        .filter((log: LogEntry) => types.includes(log.type))
        .slice(-limit);
      
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(logs)
          }
        ]
      };
    }
  );
  
  // Add tool to send key press
  server.tool(
    'sendKeyPress',
    {
      key: z.string()
    },
    async ({ key }) => {
      if (commandRunner.getStatus() !== ProcessStatus.RUNNING) {
        return {
          content: [
            {
              type: 'text',
              text: 'Command is not running'
            }
          ],
          isError: true
        };
      }
      
      // Convert special key names to actual characters if needed
      const keyMap: Record<string, string> = {
        'enter': '\r',
        'return': '\r',
        'space': ' ',
        'tab': '\t',
        'escape': '\x1b',
        'backspace': '\x7f'
      };

      const keyToSend = keyMap[key.toLowerCase()] || key;
      commandRunner.write(keyToSend);
      
      return {
        content: [
          {
            type: 'text',
            text: 'Key sent successfully'
          }
        ]
      };
    }
  );
  
  // Add tool to get process status
  server.tool(
    'getProcessStatus',
    {},
    async () => {
      const status = commandRunner.getStatus();
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify({ status })
          }
        ]
      };
    }
  );
  let transport: SSEServerTransport;
  
  // Set up SSE endpoint
  app.get("/sse", async (req, res) => {
    console.log(`[${prefix}] SSE endpoint connected`);
    if(!transport) {
      transport = new SSEServerTransport("/messages", res);
    }
    await server.connect(transport);
  });

  app.post("/messages", async (req, res) => {
    await transport.handlePostMessage(req, res);
    console.log(`[${prefix}] Message received:`, req.body);
  });
    
  // Setup raw mode for stdin
  if (process.stdin.isTTY) {
    process.stdin.setRawMode(true);
    process.stdin.resume();
    process.stdin.setEncoding('utf8');
    
    console.log(`[${prefix}] Terminal is in TTY mode, listening for keypresses`);
    
    process.stdin.on('data', (data: Buffer | string) => {
      // Convert buffer to string if needed
      const str = Buffer.isBuffer(data) ? data.toString() : data;
      
      console.log(`[${prefix}] Received keypress:`, str.split('').map((c: string) => c.charCodeAt(0)));
      
      // Handle special keys
      if (str === '\u0003') { // Ctrl+C
        console.log(`[${prefix}] Received Ctrl+C, exiting...`);
        process.exit();
      }
      
      // Map some common keys
      const keyMap: Record<string, string> = {
        '\u001b[A': '\x1b[A', // Up arrow
        '\u001b[B': '\x1b[B', // Down arrow
        '\u001b[C': '\x1b[C', // Right arrow
        '\u001b[D': '\x1b[D', // Left arrow
        '\r': '\r\n',         // Enter
      };
      
      // Forward keypress to the child process
      if (commandRunner.getStatus() === ProcessStatus.RUNNING) {
        const mapped = keyMap[str] || str;
        console.log(`[${prefix}] Forwarding keypress to child process:`, mapped.split('').map((c: string) => c.charCodeAt(0)));
        commandRunner.write(mapped);
      }
    });
    
    // Handle process exit
    process.on('exit', () => {
      if (process.stdin.isTTY) {
        process.stdin.setRawMode(false);
      }
    });
  } else {
    console.log(`[${prefix}] Terminal is not in TTY mode, keypresses won't be captured`);
  }
  
  // Start the command
  commandRunner.start();
  
  // Start the HTTP server
  const server_instance = app.listen(port, () => {
    console.log(`[${prefix}] MCP server listening on port ${port}`);
    console.log(`[${prefix}] SSE endpoint: http://localhost:${port}/sse`);
    console.log(`[${prefix}] Messages endpoint: http://localhost:${port}/messages`);
    console.log(`[${prefix}] MCP server started with command: ${command}`);
  });
  
  // Return a stop function
  return {
    stop: () => {
      commandRunner.stop();
      server_instance.close();
      console.log(`[${prefix}] MCP server stopped`);
    }
  };
}

// Re-export other utilities
export { CommandRunner, ProcessStatus, type LogEntry } from './utils/command-runner.js';
export { CircularBuffer } from './utils/buffer.js';

// Re-export the CLI for direct execution
export * from './cli.js'; 
```