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

```
1 | {
2 |   "semi": true,
3 |   "trailingComma": "all",
4 |   "singleQuote": true,
5 |   "printWidth": 100,
6 |   "tabWidth": 2,
7 |   "endOfLine": "auto"
8 | } 
```

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

```
 1 | # Source code
 2 | src/
 3 | 
 4 | # Tests
 5 | *.test.ts
 6 | *.spec.ts
 7 | test/
 8 | tests/
 9 | __tests__/
10 | *.test.js
11 | *.spec.js
12 | coverage/
13 | 
14 | # Development files
15 | .github/
16 | .vscode/
17 | .editorconfig
18 | .eslintrc*
19 | .prettierrc*
20 | tsconfig.json
21 | jest.config.*
22 | *.tsbuildinfo
23 | 
24 | # Examples
25 | examples/
26 | 
27 | # Git files
28 | .git/
29 | .gitignore
30 | 
31 | # CI/CD
32 | .github/
33 | 
34 | # Logs
35 | logs/
36 | *.log
37 | npm-debug.log*
38 | yarn-debug.log*
39 | yarn-error.log*
40 | pnpm-debug.log*
41 | 
42 | # Environment variables
43 | .env* 
```

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

```
 1 | # Dependency directories
 2 | node_modules/
 3 | pnpm-lock.yaml
 4 | 
 5 | # Built output
 6 | dist/
 7 | build/
 8 | out/
 9 | 
10 | # TypeScript cache
11 | *.tsbuildinfo
12 | 
13 | # Environment variables
14 | .env
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 | 
20 | # IDE - VSCode
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 | 
27 | # Logs
28 | logs
29 | *.log
30 | npm-debug.log*
31 | yarn-debug.log*
32 | yarn-error.log*
33 | pnpm-debug.log*
34 | 
35 | # Test coverage
36 | coverage/
37 | 
38 | # System files
39 | .DS_Store
40 | Thumbs.db 
```

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

```json
 1 | {
 2 |   "parser": "@typescript-eslint/parser",
 3 |   "extends": [
 4 |     "eslint:recommended",
 5 |     "plugin:@typescript-eslint/recommended"
 6 |   ],
 7 |   "plugins": ["@typescript-eslint"],
 8 |   "env": {
 9 |     "node": true,
10 |     "es6": true
11 |   },
12 |   "parserOptions": {
13 |     "ecmaVersion": 2022,
14 |     "sourceType": "module"
15 |   },
16 |   "rules": {
17 |     "@typescript-eslint/explicit-function-return-type": "warn",
18 |     "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
19 |     "@typescript-eslint/no-explicit-any": "warn",
20 |     "no-console": "off"
21 |   }
22 | } 
```

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

```markdown
  1 | # MCP Command Proxy
  2 | 
  3 | 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.
  4 | 
  5 | ## How to use in Cursor (Expo example)
  6 | 
  7 | 1. Go to the directory of your Expo project
  8 | 2. Run `npx mcp-command-proxy --prefix "ExpoServer" --command "expo start" --port 8383`
  9 | 3. Go to Cursor settings -> MCP -> +Add new MCP server, like this:  
 10 | ![add_mcp_server](guide/add_mcp.png)
 11 | 4. Set the name to "ExpoServer", Type to "SSE", URL to `http://localhost:8383/sse`
 12 | 5. Click "Save" and you should now be able to use the MCP server in Cursor. Like this:
 13 | ![mcp_server_in_cursor](guide/result.png)
 14 | 
 15 | Recommended to use the `--port 8383` flag to avoid conflicts with other servers.
 16 | Also, you can add following instruction to .cursorrules file:
 17 | ```
 18 | 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.
 19 | ```
 20 | 
 21 | 
 22 | ## Features
 23 | 
 24 | - **Command Proxying**: Run any CLI command through the MCP server
 25 | - **Log Collection**: Capture and store logs from running processes (configurable buffer size)
 26 | - **Key Press Forwarding**: Forward key presses from client to the running process
 27 | - **Transparent Experience**: The end user sees the command output exactly as if they ran it directly
 28 | - **Interactive Commands**: Works with interactive CLI tools like Expo
 29 | - **MCP Integration**: Built using the MCP SDK for easy integration with Claude and other MCP-enabled AI assistants
 30 | 
 31 | ## How It Works
 32 | 
 33 | 1. The server starts a specified command in a pseudo-terminal (PTY)
 34 | 2. All stdout/stderr output is:
 35 |    - Streamed to the client in real-time
 36 |    - Stored in a circular buffer (configurable size, default 300 lines)
 37 | 3. Key presses from the client are forwarded to the running process
 38 | 4. The server provides tools to:
 39 |    - View collected logs
 40 |    - Send key presses to the process
 41 |    - Get the current state of the process
 42 | 
 43 | ## Use Cases
 44 | 
 45 | - **Expo Development**: Run `expo start` and interact with it while collecting logs
 46 | - **Build Processes**: Monitor build processes and analyze logs
 47 | - **Long-running Services**: Monitor services and keep recent log history
 48 | - **Remote Command Execution**: Execute and monitor commands from remote clients
 49 | 
 50 | ## Requirements
 51 | 
 52 | - Node.js 18+ 
 53 | - TypeScript
 54 | - pnpm (recommended) or npm
 55 | 
 56 | ## Installation
 57 | 
 58 | ```bash
 59 | # Install dependencies
 60 | pnpm install
 61 | 
 62 | # Build the project
 63 | pnpm build
 64 | 
 65 | # Run directly
 66 | pnpm start -- --prefix "MyServer" --command "expo start"
 67 | 
 68 | # Or install globally
 69 | pnpm install -g
 70 | mcp-command-proxy --prefix "MyServer" --command "expo start"
 71 | ```
 72 | 
 73 | ## Usage
 74 | 
 75 | ### Basic Usage
 76 | 
 77 | ```bash
 78 | # Using the CLI
 79 | mcp-command-proxy --prefix "ExpoServer" --command "expo start"
 80 | 
 81 | # Or programmatically
 82 | import { createServer } from 'mcp-command-proxy';
 83 | 
 84 | const server = await createServer({
 85 |   prefix: 'ExpoServer',
 86 |   command: 'expo start',
 87 |   bufferSize: 500,
 88 |   port: 8080
 89 | });
 90 | 
 91 | // To stop the server later
 92 | server.stop();
 93 | ```
 94 | 
 95 | ### Options
 96 | 
 97 | - `--prefix, -p`: Name/prefix for the server (default: "CommandProxy")
 98 | - `--command, -c`: Command to run (required)
 99 | - `--buffer-size, -b`: Number of log lines to keep in memory (default: 300)
100 | - `--port`: Port for HTTP server (default: 8080)
101 | - `--help, -h`: Show help
102 | 
103 | ### MCP Integration
104 | 
105 | This server implements the following MCP tools:
106 | 
107 | 1. `getRecentLogs`: Returns the most recent logs from the buffer
108 |    - Parameters: 
109 |      - `limit` (optional): Number of logs to return (default: 100)
110 |      - `types` (optional): Types of logs to include (stdout, stderr, system) (default: all)
111 | 
112 | 2. `sendKeyPress`: Sends a key press to the running process
113 |    - Parameters:
114 |      - `key`: Key to send (e.g. "enter", "a", "1", "space")
115 | 
116 | 3. `getProcessStatus`: Returns the current status of the process
117 |    - Parameters: None
118 | 
119 | ## Examples
120 | 
121 | ### Running Expo Start
122 | 
123 | ```bash
124 | mcp-command-proxy -p "ExpoServer" -c "expo start" -b 500
125 | ```
126 | 
127 | ### Using with Claude
128 | 
129 | 1. Configure Claude to connect to this MCP server (SSE endpoint: http://localhost:8080/sse)
130 | 2. Ask Claude to run Expo or other commands
131 | 3. Claude can analyze logs and help troubleshoot issues
132 | 
133 | ## Development
134 | 
135 | ```bash
136 | # Clone the repository
137 | git clone https://github.com/hormold/mcp-command-proxy.git
138 | cd mcp-command-proxy
139 | 
140 | # Install dependencies
141 | pnpm install
142 | 
143 | # Build the project
144 | pnpm build
145 | 
146 | # Run in development mode
147 | pnpm dev
148 | ```
149 | 
150 | ## License
151 | 
152 | MIT 
```

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

```markdown
 1 | # Contributing to MCP Command Proxy
 2 | 
 3 | Thank you for your interest in contributing to MCP Command Proxy! This document provides guidelines and instructions for contributing.
 4 | 
 5 | ## Code of Conduct
 6 | 
 7 | Please be respectful and considerate of others when contributing to this project.
 8 | 
 9 | ## How Can I Contribute?
10 | 
11 | ### Reporting Bugs
12 | 
13 | When reporting bugs, please include:
14 | 
15 | - A clear and descriptive title
16 | - Steps to reproduce the issue
17 | - Expected behavior
18 | - Actual behavior
19 | - Screenshots if applicable
20 | - Your environment details (OS, Node.js version, etc.)
21 | 
22 | ### Suggesting Enhancements
23 | 
24 | Enhancement suggestions are welcome! Please provide:
25 | 
26 | - A clear and descriptive title
27 | - A detailed description of the proposed enhancement
28 | - Any relevant examples or mock-ups
29 | 
30 | ### Pull Requests
31 | 
32 | 1. Fork the repository
33 | 2. Create a new branch for your feature or bug fix
34 | 3. Write your code, with tests if applicable
35 | 4. Ensure all tests pass
36 | 5. Submit a pull request with a clear description of the changes
37 | 
38 | Please make sure your code follows our style guidelines:
39 | 
40 | - Use TypeScript
41 | - Format code with Prettier
42 | - Follow ESLint rules
43 | - Write meaningful commit messages
44 | 
45 | ## Development Setup
46 | 
47 | ```bash
48 | # Clone your fork of the repo
49 | git clone https://github.com/YOUR_USERNAME/mcp-command-proxy.git
50 | 
51 | # Navigate to the project directory
52 | cd mcp-command-proxy
53 | 
54 | # Install dependencies
55 | pnpm install
56 | 
57 | # Build the project
58 | pnpm build
59 | 
60 | # Run tests
61 | pnpm test
62 | ```
63 | 
64 | ## Project Structure
65 | 
66 | - `src/` - Source code
67 |   - `index.ts` - Main entry point for library
68 |   - `cli.ts` - CLI entry point
69 |   - `utils/` - Utility functions
70 |     - `command-runner.ts` - Core command running functionality
71 |     - `buffer.ts` - Circular buffer implementation
72 | - `dist/` - Compiled JavaScript code
73 | 
74 | ## License
75 | 
76 | By contributing, you agree that your contributions will be licensed under the project's [MIT License](LICENSE). 
```

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

```markdown
 1 | ## Description
 2 | <!-- A clear and concise description of the issue -->
 3 | 
 4 | ## Steps to Reproduce
 5 | <!-- Steps to reproduce the behavior -->
 6 | 1. 
 7 | 2. 
 8 | 3. 
 9 | 
10 | ## Expected Behavior
11 | <!-- What you expected to happen -->
12 | 
13 | ## Actual Behavior
14 | <!-- What actually happened -->
15 | 
16 | ## Environment
17 | - OS: <!-- e.g. macOS, Windows, Linux -->
18 | - Node.js version: <!-- e.g. 18.0.0 -->
19 | - MCP Command Proxy version: <!-- e.g. 1.0.0 -->
20 | 
21 | ## Additional Context
22 | <!-- Any other context, screenshots, or relevant information about the issue --> 
```

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

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

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

```javascript
 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
 2 | export default {
 3 |   preset: 'ts-jest/presets/default-esm',
 4 |   testEnvironment: 'node',
 5 |   roots: ['<rootDir>/src'],
 6 |   testMatch: ['**/*.test.ts'],
 7 |   transform: {
 8 |     '^.+\\.tsx?$': [
 9 |       'ts-jest',
10 |       {
11 |         useESM: true,
12 |       },
13 |     ],
14 |   },
15 |   moduleNameMapper: {
16 |     '^@/(.*)$': '<rootDir>/src/$1',
17 |     '^(\\.{1,2}/.*)\\.js$': '$1',
18 |   },
19 |   collectCoverageFrom: [
20 |     'src/**/*.{ts,tsx}',
21 |     '!src/**/*.d.ts',
22 |     '!src/**/*.test.{ts,tsx}',
23 |   ],
24 |   extensionsToTreatAsEsm: ['.ts', '.tsx'],
25 |   moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
26 | }; 
```

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

```yaml
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ main ]
 6 |   pull_request:
 7 |     branches: [ main ]
 8 | 
 9 | jobs:
10 |   build:
11 |     runs-on: ubuntu-latest
12 | 
13 |     strategy:
14 |       matrix:
15 |         node-version: [18.x, 20.x]
16 | 
17 |     steps:
18 |     - uses: actions/checkout@v3
19 |     
20 |     - name: Use Node.js ${{ matrix.node-version }}
21 |       uses: actions/setup-node@v3
22 |       with:
23 |         node-version: ${{ matrix.node-version }}
24 |         
25 |     - name: Setup PNPM
26 |       uses: pnpm/action-setup@v2
27 |       with:
28 |         version: 8
29 |         
30 |     - name: Install dependencies
31 |       run: pnpm install
32 |       
33 |     - name: Run linter
34 |       run: pnpm lint
35 |       
36 |     - name: Run tests
37 |       run: pnpm test
38 |       
39 |     - name: Build
40 |       run: pnpm build 
```

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

```yaml
 1 | name: Publish to NPM
 2 | 
 3 | on:
 4 |   release:
 5 |     types: [created]
 6 | 
 7 | jobs:
 8 |   publish:
 9 |     runs-on: ubuntu-latest
10 |     permissions:
11 |       contents: read
12 |       packages: write
13 |     steps:
14 |       - uses: actions/checkout@v3
15 |       
16 |       - name: Setup Node.js
17 |         uses: actions/setup-node@v3
18 |         with:
19 |           node-version: '20.x'
20 |           registry-url: 'https://registry.npmjs.org/'
21 |           
22 |       - name: Setup PNPM
23 |         uses: pnpm/action-setup@v2
24 |         with:
25 |           version: 8
26 |           
27 |       - name: Install dependencies
28 |         run: pnpm install
29 |         
30 |       - name: Run tests
31 |         run: pnpm test
32 |         
33 |       - name: Build
34 |         run: pnpm build
35 |         
36 |       - name: Publish to NPM
37 |         run: pnpm publish --no-git-checks
38 |         env:
39 |           NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
40 |           NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
41 |           PNPM_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 
```

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

```javascript
 1 | /**
 2 |  * Basic usage example for mcp-command-proxy
 3 |  * 
 4 |  * This example shows how to create an MCP server that runs the "echo" command
 5 |  * and demonstrates how to interact with it programmatically.
 6 |  * 
 7 |  * @copyright 2025 Hormold
 8 |  * @license MIT
 9 |  */
10 | 
11 | import { createServer } from 'mcp-command-proxy';
12 | 
13 | // Create a server that runs "echo Hello, MCP!"
14 | const server = await createServer({
15 |   prefix: 'ExampleServer',
16 |   command: 'echo "Hello, MCP!"',
17 |   bufferSize: 100,
18 |   port: 8080
19 | });
20 | 
21 | console.log('MCP server started. Press Ctrl+C to exit.');
22 | 
23 | // Handle exit signals
24 | process.on('SIGINT', () => {
25 |   console.log('Shutting down MCP server...');
26 |   server.stop();
27 |   process.exit(0);
28 | });
29 | 
30 | /**
31 |  * In a real application, you might want to do something with the server
32 |  * For example, you could expose it via a REST API or use it for automation
33 |  * 
34 |  * Example of accessing logs programmatically:
35 |  * 
36 |  * import { CommandRunner } from 'mcp-command-proxy';
37 |  * 
38 |  * // Create a command runner directly
39 |  * const runner = new CommandRunner({
40 |  *   command: 'npm start',
41 |  *   logBufferSize: 500
42 |  * });
43 |  * 
44 |  * // Handle logs
45 |  * runner.on('log', (entry) => {
46 |  *   console.log(`[${entry.type}] ${entry.content}`);
47 |  * });
48 |  * 
49 |  * // Start the command
50 |  * runner.start();
51 |  * 
52 |  * // Send input
53 |  * runner.write('y\n');
54 |  * 
55 |  * // Get logs
56 |  * const logs = runner.getLogs();
57 |  * 
58 |  * // Stop the command
59 |  * runner.stop();
60 |  */ 
```

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

```typescript
 1 | /**
 2 |  * A circular buffer implementation for storing log lines with a fixed capacity
 3 |  */
 4 | export class CircularBuffer<T> {
 5 |   private buffer: T[];
 6 |   private head: number = 0;
 7 |   private tail: number = 0;
 8 |   private size: number = 0;
 9 |   private readonly capacity: number;
10 | 
11 |   /**
12 |    * Create a new circular buffer
13 |    * @param capacity Maximum number of items the buffer can hold
14 |    */
15 |   constructor(capacity: number) {
16 |     this.buffer = new Array<T>(capacity);
17 |     this.capacity = capacity;
18 |   }
19 | 
20 |   /**
21 |    * Add an item to the buffer, overwriting the oldest item if full
22 |    * @param item Item to add to the buffer
23 |    */
24 |   push(item: T): void {
25 |     this.buffer[this.head] = item;
26 |     this.head = (this.head + 1) % this.capacity;
27 | 
28 |     if (this.size < this.capacity) {
29 |       this.size++;
30 |     } else {
31 |       // Buffer is full, move tail forward
32 |       this.tail = (this.tail + 1) % this.capacity;
33 |     }
34 |   }
35 | 
36 |   /**
37 |    * Get all items currently in the buffer in order of insertion
38 |    * @returns Array of items in order of insertion (oldest to newest)
39 |    */
40 |   getAll(): T[] {
41 |     const result: T[] = [];
42 |     let current = this.tail;
43 | 
44 |     for (let i = 0; i < this.size; i++) {
45 |       result.push(this.buffer[current]);
46 |       current = (current + 1) % this.capacity;
47 |     }
48 | 
49 |     return result;
50 |   }
51 | 
52 |   /**
53 |    * Get the number of items in the buffer
54 |    */
55 |   getSize(): number {
56 |     return this.size;
57 |   }
58 | 
59 |   /**
60 |    * Get the capacity of the buffer
61 |    */
62 |   getCapacity(): number {
63 |     return this.capacity;
64 |   }
65 | 
66 |   /**
67 |    * Clear all items from the buffer
68 |    */
69 |   clear(): void {
70 |     this.head = 0;
71 |     this.tail = 0;
72 |     this.size = 0;
73 |   }
74 | } 
```

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

```typescript
 1 | import { CircularBuffer } from './buffer.js';
 2 | 
 3 | describe('CircularBuffer', () => {
 4 |   describe('constructor', () => {
 5 |     it('should create a buffer with the specified capacity', () => {
 6 |       const buffer = new CircularBuffer<number>(5);
 7 |       expect(buffer.getCapacity()).toBe(5);
 8 |       expect(buffer.getSize()).toBe(0);
 9 |     });
10 |   });
11 | 
12 |   describe('push', () => {
13 |     it('should add items to the buffer', () => {
14 |       const buffer = new CircularBuffer<number>(3);
15 |       buffer.push(1);
16 |       buffer.push(2);
17 |       expect(buffer.getSize()).toBe(2);
18 |       expect(buffer.getAll()).toEqual([1, 2]);
19 |     });
20 | 
21 |     it('should overwrite old items when full', () => {
22 |       const buffer = new CircularBuffer<number>(3);
23 |       buffer.push(1);
24 |       buffer.push(2);
25 |       buffer.push(3);
26 |       buffer.push(4);
27 |       expect(buffer.getSize()).toBe(3);
28 |       expect(buffer.getAll()).toEqual([2, 3, 4]);
29 |     });
30 |   });
31 | 
32 |   describe('getAll', () => {
33 |     it('should return all items in order', () => {
34 |       const buffer = new CircularBuffer<string>(5);
35 |       buffer.push('a');
36 |       buffer.push('b');
37 |       buffer.push('c');
38 |       expect(buffer.getAll()).toEqual(['a', 'b', 'c']);
39 |     });
40 | 
41 |     it('should return an empty array for an empty buffer', () => {
42 |       const buffer = new CircularBuffer<string>(5);
43 |       expect(buffer.getAll()).toEqual([]);
44 |     });
45 |   });
46 | 
47 |   describe('clear', () => {
48 |     it('should remove all items from the buffer', () => {
49 |       const buffer = new CircularBuffer<number>(5);
50 |       buffer.push(1);
51 |       buffer.push(2);
52 |       buffer.push(3);
53 |       buffer.clear();
54 |       expect(buffer.getSize()).toBe(0);
55 |       expect(buffer.getAll()).toEqual([]);
56 |     });
57 |   });
58 | }); 
```

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

```json
  1 | {
  2 |   "name": "mcp-command-proxy",
  3 |   "version": "1.0.2",
  4 |   "description": "MCP server that proxies CLI commands and collects logs",
  5 |   "type": "module",
  6 |   "main": "dist/index.js",
  7 |   "bin": {
  8 |     "mcp-command-proxy": "dist/cli.js"
  9 |   },
 10 |   "scripts": {
 11 |     "build": "tsc",
 12 |     "start": "node dist/cli.js",
 13 |     "dev": "ts-node --esm src/cli.ts",
 14 |     "test": "jest",
 15 |     "lint": "eslint . --ext .ts",
 16 |     "format": "prettier --write \"src/**/*.ts\"",
 17 |     "prepublishOnly": "npm run build",
 18 |     "prepare": "npm run build"
 19 |   },
 20 |   "keywords": [
 21 |     "mcp",
 22 |     "cli",
 23 |     "proxy",
 24 |     "logs",
 25 |     "expo",
 26 |     "model-context-protocol"
 27 |   ],
 28 |   "author": "Hormold",
 29 |   "repository": {
 30 |     "type": "git",
 31 |     "url": "https://github.com/hormold/mcp-command-proxy.git"
 32 |   },
 33 |   "bugs": {
 34 |     "url": "https://github.com/hormold/mcp-command-proxy/issues"
 35 |   },
 36 |   "homepage": "https://github.com/hormold/mcp-command-proxy#readme",
 37 |   "license": "MIT",
 38 |   "devDependencies": {
 39 |     "@types/express": "^5.0.0",
 40 |     "@types/jest": "^29.5.11",
 41 |     "@types/node": "^20.10.4",
 42 |     "@typescript-eslint/eslint-plugin": "^6.13.2",
 43 |     "@typescript-eslint/parser": "^6.13.2",
 44 |     "eslint": "^8.55.0",
 45 |     "jest": "^29.7.0",
 46 |     "prettier": "^3.1.0",
 47 |     "ts-jest": "^29.1.1",
 48 |     "ts-node": "^10.9.1",
 49 |     "typescript": "^5.3.3"
 50 |   },
 51 |   "dependencies": {
 52 |     "@modelcontextprotocol/sdk": "^1.6.1",
 53 |     "express": "^4.21.2",
 54 |     "node-pty": "^1.0.0",
 55 |     "zod": "^3.24.2"
 56 |   },
 57 |   "mcp": {
 58 |     "name": "Command Proxy",
 59 |     "description": "MCP server that proxies CLI commands and collects logs",
 60 |     "transportType": "sse",
 61 |     "sse": {
 62 |       "endpoint": "/sse",
 63 |       "messagesEndpoint": "/messages"
 64 |     },
 65 |     "capabilities": {
 66 |       "resources": true,
 67 |       "tools": true
 68 |     },
 69 |     "tools": [
 70 |       {
 71 |         "name": "getRecentLogs",
 72 |         "description": "Get the most recent logs from the running command",
 73 |         "parameters": {
 74 |           "limit": "number?",
 75 |           "types": "string[]?"
 76 |         }
 77 |       },
 78 |       {
 79 |         "name": "sendKeyPress",
 80 |         "description": "Send a key press to the running command",
 81 |         "parameters": {
 82 |           "key": "string"
 83 |         }
 84 |       },
 85 |       {
 86 |         "name": "getProcessStatus",
 87 |         "description": "Get the current status of the running command",
 88 |         "parameters": {}
 89 |       }
 90 |     ],
 91 |     "resources": [
 92 |       {
 93 |         "name": "server-info",
 94 |         "description": "Information about the server and available tools",
 95 |         "uri": "server://info"
 96 |       }
 97 |     ]
 98 |   },
 99 |   "engines": {
100 |     "node": ">=18.0.0"
101 |   },
102 |   "files": [
103 |     "dist",
104 |     "README.md",
105 |     "LICENSE"
106 |   ]
107 | }
```

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

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { createServer } from './index.js';
  4 | 
  5 | // Parse command line arguments
  6 | export function parseArgs(): { prefix: string; command: string; bufferSize: number; port: number } {
  7 |   const args = process.argv.slice(2);
  8 |   let prefix = 'CommandProxy';
  9 |   let command = '';
 10 |   let bufferSize = 300;
 11 |   let port = 8080;
 12 | 
 13 |   for (let i = 0; i < args.length; i++) {
 14 |     const arg = args[i];
 15 |     
 16 |     if (arg === '--prefix' || arg === '-p') {
 17 |       prefix = args[++i] || prefix;
 18 |     } else if (arg === '--command' || arg === '-c') {
 19 |       command = args[++i] || command;
 20 |     } else if (arg === '--buffer-size' || arg === '-b') {
 21 |       bufferSize = parseInt(args[++i] || String(bufferSize), 10);
 22 |     } else if (arg === '--port') {
 23 |       port = parseInt(args[++i] || String(port), 10);
 24 |     } else if (arg === '--help' || arg === '-h') {
 25 |       showHelp();
 26 |       process.exit(0);
 27 |     }
 28 |   }
 29 | 
 30 |   if (!command) {
 31 |     console.error('Error: Command is required');
 32 |     showHelp();
 33 |     process.exit(1);
 34 |   }
 35 | 
 36 |   return { prefix, command, bufferSize, port };
 37 | }
 38 | 
 39 | export function showHelp(): void {
 40 |   console.log(`
 41 | MCP Command Proxy - Run CLI commands with MCP
 42 | 
 43 | Usage:
 44 |   mcp-command-proxy [options]
 45 | 
 46 | Options:
 47 |   --prefix, -p        Name/prefix for the server (default: "CommandProxy")
 48 |   --command, -c       Command to run (required)
 49 |   --buffer-size, -b   Number of log lines to keep in memory (default: 300)
 50 |   --port              Port for HTTP server (default: 8080)
 51 |   --help, -h          Show this help message
 52 | 
 53 | Example:
 54 |   mcp-command-proxy -p "ExpoServer" -c "expo start" -b 500 --port 8080
 55 |   `);
 56 | }
 57 | 
 58 | // Main function
 59 | export async function main(): Promise<void> {
 60 |   try {
 61 |     const { prefix, command, bufferSize, port } = parseArgs();
 62 |     
 63 |     console.log(`Starting MCP Command Proxy with:
 64 |   - Prefix: ${prefix}
 65 |   - Command: ${command}
 66 |   - Buffer Size: ${bufferSize}
 67 |   - Port: ${port}
 68 | `);
 69 | 
 70 |     const server = await createServer({
 71 |       prefix,
 72 |       command,
 73 |       bufferSize,
 74 |       port
 75 |     });
 76 | 
 77 |     // Handle exit signals
 78 |     const exitHandler = (): void => {
 79 |       console.log('\nShutting down MCP Command Proxy...');
 80 |       server.stop();
 81 |       process.exit(0);
 82 |     };
 83 | 
 84 |     process.on('SIGINT', exitHandler);
 85 |     process.on('SIGTERM', exitHandler);
 86 |     
 87 |     console.log(`
 88 | MCP Command Proxy is running!
 89 | - SSE endpoint: http://localhost:${port}/sse
 90 | - Messages endpoint: http://localhost:${port}/messages
 91 | 
 92 | Connect your MCP client to these endpoints.
 93 | `);
 94 |   } catch (error) {
 95 |     console.error('Error starting MCP Command Proxy:', error);
 96 |     process.exit(1);
 97 |   }
 98 | }
 99 | 
100 | // Only run main if this is the entry point
101 | main().catch(error => {
102 |   console.error('Unhandled error:', error);
103 |   process.exit(1);
104 | }); 
```

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

```typescript
  1 | import * as pty from 'node-pty';
  2 | import { EventEmitter } from 'events';
  3 | import { CircularBuffer } from './buffer.js';
  4 | 
  5 | /**
  6 |  * Log entry type for storing command output
  7 |  */
  8 | export interface LogEntry {
  9 |   timestamp: number;
 10 |   content: string;
 11 |   type: 'stdout' | 'stderr' | 'system';
 12 | }
 13 | 
 14 | /**
 15 |  * Process status type
 16 |  */
 17 | export enum ProcessStatus {
 18 |   RUNNING = 'running',
 19 |   STOPPED = 'stopped',
 20 |   ERROR = 'error',
 21 | }
 22 | 
 23 | /**
 24 |  * Events emitted by the CommandRunner
 25 |  */
 26 | export interface CommandRunnerEvents {
 27 |   log: (entry: LogEntry) => void;
 28 |   exit: (code: number, signal?: string) => void;
 29 |   error: (error: Error) => void;
 30 |   statusChange: (status: ProcessStatus) => void;
 31 | }
 32 | 
 33 | /**
 34 |  * CommandRunner options
 35 |  */
 36 | export interface CommandRunnerOptions {
 37 |   command: string;
 38 |   args?: string[];
 39 |   cwd?: string;
 40 |   env?: NodeJS.ProcessEnv;
 41 |   logBufferSize?: number;
 42 | }
 43 | 
 44 | /**
 45 |  * Class that runs a command in a pseudo-terminal and captures output
 46 |  */
 47 | export class CommandRunner extends EventEmitter {
 48 |   private process: pty.IPty | null = null;
 49 |   private logBuffer: CircularBuffer<LogEntry>;
 50 |   private status: ProcessStatus = ProcessStatus.STOPPED;
 51 |   private readonly command: string;
 52 |   private readonly args: string[];
 53 |   private readonly cwd: string;
 54 |   private readonly env: NodeJS.ProcessEnv;
 55 | 
 56 |   /**
 57 |    * Create a new CommandRunner
 58 |    */
 59 |   constructor(options: CommandRunnerOptions) {
 60 |     super();
 61 |     
 62 |     // Parse command and arguments
 63 |     const parts = options.command.split(' ');
 64 |     this.command = parts[0];
 65 |     this.args = parts.slice(1).concat(options.args || []);
 66 |     
 67 |     this.cwd = options.cwd || process.cwd();
 68 |     this.env = options.env || process.env;
 69 |     this.logBuffer = new CircularBuffer<LogEntry>(options.logBufferSize || 300);
 70 |   }
 71 | 
 72 |   /**
 73 |    * Start the command process
 74 |    */
 75 |   start(): void {
 76 |     try {
 77 |       // Add a system log entry
 78 |       this.addLogEntry(`Starting command: ${this.command} ${this.args.join(' ')}`, 'system');
 79 |       
 80 |       // Spawn the process
 81 |       this.process = pty.spawn(this.command, this.args, {
 82 |         name: 'xterm-color',
 83 |         cols: 80,
 84 |         rows: 30,
 85 |         cwd: this.cwd,
 86 |         env: { ...this.env, FORCE_COLOR: '1', TERM: 'xterm-256color' },
 87 |         handleFlowControl: true,
 88 |       });
 89 | 
 90 |       // Set status to running
 91 |       this.setStatus(ProcessStatus.RUNNING);
 92 | 
 93 |       // Handle data events (output)
 94 |       this.process.onData((data) => {
 95 |         this.addLogEntry(data, 'stdout');
 96 |       });
 97 | 
 98 |       // Handle exit events
 99 |       this.process.onExit(({ exitCode, signal }) => {
100 |         this.addLogEntry(`Process exited with code ${exitCode} and signal ${signal || 'none'}`, 'system');
101 |         this.setStatus(ProcessStatus.STOPPED);
102 |         this.emit('exit', exitCode, signal);
103 |         this.process = null;
104 |       });
105 |     } catch (error) {
106 |       this.setStatus(ProcessStatus.ERROR);
107 |       this.addLogEntry(`Error starting command: ${error}`, 'system');
108 |       this.emit('error', error);
109 |     }
110 |   }
111 | 
112 |   /**
113 |    * Stop the command process
114 |    */
115 |   stop(): void {
116 |     if (this.process && this.status === ProcessStatus.RUNNING) {
117 |       this.addLogEntry('Stopping command...', 'system');
118 |       this.process.kill();
119 |     }
120 |   }
121 | 
122 |   /**
123 |    * Send data (key presses) to the process
124 |    */
125 |   write(data: string): void {
126 |     if (this.process && this.status === ProcessStatus.RUNNING) {
127 |       this.addLogEntry(`Attempting to write key: ${JSON.stringify(data)}`, 'system');
128 |       try {
129 |         this.process.write(data);
130 |         this.addLogEntry(`Successfully wrote key`, 'system');
131 |       } catch (err) {
132 |         this.addLogEntry(`Failed to write key: ${err}`, 'system');
133 |       }
134 |     } else {
135 |       this.addLogEntry('Cannot write to process: not running', 'system');
136 |     }
137 |   }
138 | 
139 |   /**
140 |    * Get all log entries
141 |    */
142 |   getLogs(): LogEntry[] {
143 |     return this.logBuffer.getAll();
144 |   }
145 | 
146 |   /**
147 |    * Get current process status
148 |    */
149 |   getStatus(): ProcessStatus {
150 |     return this.status;
151 |   }
152 | 
153 |   /**
154 |    * Add a log entry to the buffer and emit a log event
155 |    */
156 |   private addLogEntry(content: string, type: LogEntry['type']): void {
157 |     const entry: LogEntry = {
158 |       timestamp: Date.now(),
159 |       content,
160 |       type,
161 |     };
162 |     
163 |     this.logBuffer.push(entry);
164 |     this.emit('log', entry);
165 |   }
166 | 
167 |   /**
168 |    * Set the process status and emit a status change event
169 |    */
170 |   private setStatus(status: ProcessStatus): void {
171 |     this.status = status;
172 |     this.emit('statusChange', this.status);
173 |   }
174 | } 
```

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

```typescript
  1 | import { CommandRunner, ProcessStatus } from './command-runner';
  2 | import * as pty from 'node-pty';
  3 | 
  4 | // Mock node-pty
  5 | jest.mock('node-pty', () => ({
  6 |   spawn: jest.fn(() => ({
  7 |     onData: jest.fn(),
  8 |     onExit: jest.fn(),
  9 |     write: jest.fn(),
 10 |     kill: jest.fn(),
 11 |   })),
 12 | }));
 13 | 
 14 | describe('CommandRunner', () => {
 15 |   let runner: CommandRunner;
 16 |   let mockProcess: jest.Mocked<pty.IPty>;
 17 | 
 18 |   beforeEach(() => {
 19 |     jest.clearAllMocks();
 20 |     mockProcess = {
 21 |       onData: jest.fn(),
 22 |       onExit: jest.fn(),
 23 |       write: jest.fn(),
 24 |       kill: jest.fn(),
 25 |     } as unknown as jest.Mocked<pty.IPty>;
 26 |     (pty.spawn as jest.Mock).mockReturnValue(mockProcess);
 27 |   });
 28 | 
 29 |   describe('constructor', () => {
 30 |     it('should initialize with default options', () => {
 31 |       runner = new CommandRunner({ command: 'test-cmd' });
 32 |       expect(runner.getStatus()).toBe(ProcessStatus.STOPPED);
 33 |       expect(runner.getLogs()).toEqual([]);
 34 |     });
 35 | 
 36 |     it('should parse command and arguments correctly', () => {
 37 |       runner = new CommandRunner({ 
 38 |         command: 'test-cmd arg1 arg2',
 39 |         args: ['arg3']
 40 |       });
 41 |       runner.start();
 42 |       expect(pty.spawn).toHaveBeenCalledWith(
 43 |         'test-cmd',
 44 |         ['arg1', 'arg2', 'arg3'],
 45 |         expect.any(Object)
 46 |       );
 47 |     });
 48 | 
 49 |     it('should use provided options', () => {
 50 |       const cwd = '/test/dir';
 51 |       const env = { TEST_ENV: 'value' };
 52 |       runner = new CommandRunner({ 
 53 |         command: 'test-cmd',
 54 |         cwd,
 55 |         env,
 56 |         logBufferSize: 100
 57 |       });
 58 |       runner.start();
 59 |       expect(pty.spawn).toHaveBeenCalledWith(
 60 |         'test-cmd',
 61 |         [],
 62 |         expect.objectContaining({
 63 |           cwd,
 64 |           env: expect.objectContaining(env)
 65 |         })
 66 |       );
 67 |     });
 68 |   });
 69 | 
 70 |   describe('process lifecycle', () => {
 71 |     beforeEach(() => {
 72 |       runner = new CommandRunner({ command: 'test-cmd' });
 73 |     });
 74 | 
 75 |     it('should start process and update status', () => {
 76 |       const statusListener = jest.fn();
 77 |       runner.on('statusChange', statusListener);
 78 |       
 79 |       runner.start();
 80 |       
 81 |       expect(runner.getStatus()).toBe(ProcessStatus.RUNNING);
 82 |       expect(statusListener).toHaveBeenCalledWith(ProcessStatus.RUNNING);
 83 |       expect(pty.spawn).toHaveBeenCalled();
 84 |     });
 85 | 
 86 |     it('should handle process exit', () => {
 87 |       const exitListener = jest.fn();
 88 |       const statusListener = jest.fn();
 89 |       runner.on('exit', exitListener);
 90 |       runner.on('statusChange', statusListener);
 91 |       
 92 |       runner.start();
 93 |       const exitCallback = (mockProcess.onExit as jest.Mock).mock.calls[0][0];
 94 |       exitCallback({ exitCode: 0, signal: null });
 95 |       
 96 |       expect(runner.getStatus()).toBe(ProcessStatus.STOPPED);
 97 |       expect(exitListener).toHaveBeenCalledWith(0, null);
 98 |       expect(statusListener).toHaveBeenCalledWith(ProcessStatus.STOPPED);
 99 |     });
100 | 
101 |     it('should stop process', () => {
102 |       runner.start();
103 |       runner.stop();
104 |       
105 |       expect(mockProcess.kill).toHaveBeenCalled();
106 |     });
107 |   });
108 | 
109 |   describe('log management', () => {
110 |     beforeEach(() => {
111 |       runner = new CommandRunner({ command: 'test-cmd' });
112 |     });
113 | 
114 |     it('should capture stdout', () => {
115 |       const logListener = jest.fn();
116 |       runner.on('log', logListener);
117 |       
118 |       runner.start();
119 |       const dataCallback = (mockProcess.onData as jest.Mock).mock.calls[0][0];
120 |       dataCallback('test output');
121 |       
122 |       const logs = runner.getLogs();
123 |       expect(logs).toHaveLength(2); // Including start command log
124 |       expect(logs[1].content).toBe('test output');
125 |       expect(logs[1].type).toBe('stdout');
126 |       expect(logListener).toHaveBeenCalledWith(expect.objectContaining({
127 |         content: 'test output',
128 |         type: 'stdout'
129 |       }));
130 |     });
131 | 
132 |     it('should respect log buffer size', () => {
133 |       runner = new CommandRunner({ 
134 |         command: 'test-cmd',
135 |         logBufferSize: 2
136 |       });
137 |       
138 |       runner.start();
139 |       const dataCallback = (mockProcess.onData as jest.Mock).mock.calls[0][0];
140 |       dataCallback('output1');
141 |       dataCallback('output2');
142 |       dataCallback('output3');
143 |       
144 |       const logs = runner.getLogs();
145 |       expect(logs).toHaveLength(2);
146 |       expect(logs.map(l => l.content)).toEqual(['output2', 'output3']);
147 |     });
148 |   });
149 | 
150 |   describe('error handling', () => {
151 |     it('should handle spawn errors', () => {
152 |       const error = new Error('Spawn error');
153 |       (pty.spawn as jest.Mock).mockImplementation(() => {
154 |         throw error;
155 |       });
156 | 
157 |       const errorListener = jest.fn();
158 |       runner = new CommandRunner({ command: 'test-cmd' });
159 |       runner.on('error', errorListener);
160 |       
161 |       runner.start();
162 |       
163 |       expect(runner.getStatus()).toBe(ProcessStatus.ERROR);
164 |       expect(errorListener).toHaveBeenCalledWith(error);
165 |     });
166 | 
167 |     it('should handle write errors', () => {
168 |       runner = new CommandRunner({ command: 'test-cmd' });
169 |       runner.start();
170 |       
171 |       mockProcess.write.mockImplementation(() => {
172 |         throw new Error('Write error');
173 |       });
174 |       
175 |       runner.write('test');
176 |       
177 |       const logs = runner.getLogs();
178 |       expect(logs.some(log => 
179 |         log.type === 'system' && 
180 |         log.content.includes('Failed to write key')
181 |       )).toBe(true);
182 |     });
183 |   });
184 | 
185 |   describe('write', () => {
186 |     beforeEach(() => {
187 |       runner = new CommandRunner({ command: 'test-cmd' });
188 |     });
189 | 
190 |     it('should write to process when running', () => {
191 |       runner.start();
192 |       runner.write('test input');
193 |       
194 |       expect(mockProcess.write).toHaveBeenCalledWith('test input');
195 |     });
196 | 
197 |     it('should not write when process is stopped', () => {
198 |       runner.write('test input');
199 |       
200 |       expect(mockProcess.write).not.toHaveBeenCalled();
201 |       const logs = runner.getLogs();
202 |       expect(logs.some(log => 
203 |         log.type === 'system' && 
204 |         log.content.includes('Cannot write to process: not running')
205 |       )).toBe(true);
206 |     });
207 |   });
208 | });
```

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

```typescript
  1 | /**
  2 |  * MCP Command Proxy
  3 |  * 
  4 |  * A Model Context Protocol (MCP) server for proxying CLI commands and collecting logs
  5 |  * 
  6 |  * @module mcp-command-proxy
  7 |  */
  8 | 
  9 | import express from 'express';
 10 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
 11 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
 12 | import { z } from 'zod';
 13 | import { CommandRunner, ProcessStatus, LogEntry } from './utils/command-runner.js';
 14 | 
 15 | /**
 16 |  * Create an MCP server for proxying CLI commands
 17 |  */
 18 | export async function createServer(options: {
 19 |   prefix: string;
 20 |   command: string;
 21 |   bufferSize?: number;
 22 |   port: number;
 23 | }) {
 24 |   const { prefix, command, bufferSize = 300, port } = options;
 25 |   
 26 |   // Create Express app
 27 |   const app = express();
 28 |   
 29 |   // Parse JSON bodies
 30 |   app.use(express.json());
 31 |   
 32 |   // Create MCP server
 33 |   const server = new McpServer({
 34 |     name: `${prefix} MCP Server`,
 35 |     version: '1.0.0'
 36 |   });
 37 |   
 38 |   // Create command runner
 39 |   const commandRunner = new CommandRunner({
 40 |     command,
 41 |     logBufferSize: bufferSize,
 42 |   });
 43 |   
 44 |   // Setup command runner event handlers
 45 |   commandRunner.on('log', (entry: LogEntry) => {
 46 |     // Log to console for debugging
 47 |     if (entry.type === 'stdout') {
 48 |       process.stdout.write(entry.content);
 49 |     } else if (entry.type === 'stderr') {
 50 |       process.stderr.write(entry.content);
 51 |     } else {
 52 |       console.log(`[${prefix}] ${entry.content}`);
 53 |     }
 54 |   });
 55 |   
 56 |   commandRunner.on('exit', (code: number) => {
 57 |     console.log(`[${prefix}] Command exited with code ${code}`);
 58 |   });
 59 |   
 60 |   commandRunner.on('error', (error: Error) => {
 61 |     console.error(`[${prefix}] Command error:`, error);
 62 |   });
 63 |   
 64 |   // Add MCP tools
 65 |   
 66 |   // Add a resource for recent logs
 67 |   server.resource(
 68 |     'logs',
 69 |     'logs://recent',
 70 |     async () => {
 71 |       const logs = commandRunner.getLogs()
 72 |         .slice(-100); // Default to 100 most recent logs
 73 |       
 74 |       return {
 75 |         contents: [{
 76 |           uri: 'logs://recent',
 77 |           text: JSON.stringify(logs, null, 2)
 78 |         }]
 79 |       };
 80 |     }
 81 |   );
 82 |   
 83 |   // Add tool to get recent logs
 84 |   server.tool(
 85 |     'getRecentLogs',
 86 |     {
 87 |       limit: z.number().optional().default(100),
 88 |       types: z.array(z.enum(['stdout', 'stderr', 'system'])).optional().default(['stdout', 'stderr', 'system'])
 89 |     },
 90 |     async ({ limit, types }) => {
 91 |       const logs = commandRunner.getLogs()
 92 |         .filter((log: LogEntry) => types.includes(log.type))
 93 |         .slice(-limit);
 94 |       
 95 |       return {
 96 |         content: [
 97 |           {
 98 |             type: 'text',
 99 |             text: JSON.stringify(logs)
100 |           }
101 |         ]
102 |       };
103 |     }
104 |   );
105 |   
106 |   // Add tool to send key press
107 |   server.tool(
108 |     'sendKeyPress',
109 |     {
110 |       key: z.string()
111 |     },
112 |     async ({ key }) => {
113 |       if (commandRunner.getStatus() !== ProcessStatus.RUNNING) {
114 |         return {
115 |           content: [
116 |             {
117 |               type: 'text',
118 |               text: 'Command is not running'
119 |             }
120 |           ],
121 |           isError: true
122 |         };
123 |       }
124 |       
125 |       // Convert special key names to actual characters if needed
126 |       const keyMap: Record<string, string> = {
127 |         'enter': '\r',
128 |         'return': '\r',
129 |         'space': ' ',
130 |         'tab': '\t',
131 |         'escape': '\x1b',
132 |         'backspace': '\x7f'
133 |       };
134 | 
135 |       const keyToSend = keyMap[key.toLowerCase()] || key;
136 |       commandRunner.write(keyToSend);
137 |       
138 |       return {
139 |         content: [
140 |           {
141 |             type: 'text',
142 |             text: 'Key sent successfully'
143 |           }
144 |         ]
145 |       };
146 |     }
147 |   );
148 |   
149 |   // Add tool to get process status
150 |   server.tool(
151 |     'getProcessStatus',
152 |     {},
153 |     async () => {
154 |       const status = commandRunner.getStatus();
155 |       return {
156 |         content: [
157 |           {
158 |             type: 'text',
159 |             text: JSON.stringify({ status })
160 |           }
161 |         ]
162 |       };
163 |     }
164 |   );
165 |   let transport: SSEServerTransport;
166 |   
167 |   // Set up SSE endpoint
168 |   app.get("/sse", async (req, res) => {
169 |     console.log(`[${prefix}] SSE endpoint connected`);
170 |     if(!transport) {
171 |       transport = new SSEServerTransport("/messages", res);
172 |     }
173 |     await server.connect(transport);
174 |   });
175 | 
176 |   app.post("/messages", async (req, res) => {
177 |     await transport.handlePostMessage(req, res);
178 |     console.log(`[${prefix}] Message received:`, req.body);
179 |   });
180 |     
181 |   // Setup raw mode for stdin
182 |   if (process.stdin.isTTY) {
183 |     process.stdin.setRawMode(true);
184 |     process.stdin.resume();
185 |     process.stdin.setEncoding('utf8');
186 |     
187 |     console.log(`[${prefix}] Terminal is in TTY mode, listening for keypresses`);
188 |     
189 |     process.stdin.on('data', (data: Buffer | string) => {
190 |       // Convert buffer to string if needed
191 |       const str = Buffer.isBuffer(data) ? data.toString() : data;
192 |       
193 |       console.log(`[${prefix}] Received keypress:`, str.split('').map((c: string) => c.charCodeAt(0)));
194 |       
195 |       // Handle special keys
196 |       if (str === '\u0003') { // Ctrl+C
197 |         console.log(`[${prefix}] Received Ctrl+C, exiting...`);
198 |         process.exit();
199 |       }
200 |       
201 |       // Map some common keys
202 |       const keyMap: Record<string, string> = {
203 |         '\u001b[A': '\x1b[A', // Up arrow
204 |         '\u001b[B': '\x1b[B', // Down arrow
205 |         '\u001b[C': '\x1b[C', // Right arrow
206 |         '\u001b[D': '\x1b[D', // Left arrow
207 |         '\r': '\r\n',         // Enter
208 |       };
209 |       
210 |       // Forward keypress to the child process
211 |       if (commandRunner.getStatus() === ProcessStatus.RUNNING) {
212 |         const mapped = keyMap[str] || str;
213 |         console.log(`[${prefix}] Forwarding keypress to child process:`, mapped.split('').map((c: string) => c.charCodeAt(0)));
214 |         commandRunner.write(mapped);
215 |       }
216 |     });
217 |     
218 |     // Handle process exit
219 |     process.on('exit', () => {
220 |       if (process.stdin.isTTY) {
221 |         process.stdin.setRawMode(false);
222 |       }
223 |     });
224 |   } else {
225 |     console.log(`[${prefix}] Terminal is not in TTY mode, keypresses won't be captured`);
226 |   }
227 |   
228 |   // Start the command
229 |   commandRunner.start();
230 |   
231 |   // Start the HTTP server
232 |   const server_instance = app.listen(port, () => {
233 |     console.log(`[${prefix}] MCP server listening on port ${port}`);
234 |     console.log(`[${prefix}] SSE endpoint: http://localhost:${port}/sse`);
235 |     console.log(`[${prefix}] Messages endpoint: http://localhost:${port}/messages`);
236 |     console.log(`[${prefix}] MCP server started with command: ${command}`);
237 |   });
238 |   
239 |   // Return a stop function
240 |   return {
241 |     stop: () => {
242 |       commandRunner.stop();
243 |       server_instance.close();
244 |       console.log(`[${prefix}] MCP server stopped`);
245 |     }
246 |   };
247 | }
248 | 
249 | // Re-export other utilities
250 | export { CommandRunner, ProcessStatus, type LogEntry } from './utils/command-runner.js';
251 | export { CircularBuffer } from './utils/buffer.js';
252 | 
253 | // Re-export the CLI for direct execution
254 | export * from './cli.js'; 
```