# 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 |  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 |  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'; ```