README.md•12.3 kB
# mcp2term
An implementation of a Model Context Protocol (MCP) server that grants safe, auditable access to a system shell. The server streams stdout and stderr in real time while capturing rich metadata for plugins and downstream consumers.
## Features
- **Full command execution** with configurable shell, working directory, environment variables, and timeouts.
- **Live streaming** of stdout and stderr via MCP log notifications so clients observe progress as it happens.
- **Robust chunked streaming** that handles large stdout/stderr volumes without blocking or truncation.
- **Plugin architecture** that exposes every function, class, and variable defined in the package, enabling extensions to observe command lifecycles or inject custom behaviour.
- **Remote file management** tools allowing safe file creation, printing, line-range replacement, exact line lookups, and unified diff patching via the `manage_file` tool and `filetool` client command.
- **Automatic ngrok tunneling** so HTTP transports are reachable without additional manual setup.
- **Typed lifespan context** shared with MCP tools for dependency access and lifecycle management.
- **Structured tool responses** including timing information to make results easy for agents to consume.
- **Console mirroring** so operators always see the command stream, stdout, and stderr on the hosting terminal by default.
- **Automatic launch-directory export** that prepends the directory the server
was started from to ``PYTHONPATH`` so Python tooling invoked through
``run_command`` can immediately resolve local packages.
## Installation
```bash
pip install -e .
```
The project targets Python 3.12 or newer.
## Configuration
`ServerConfig` reads settings from environment variables:
| Variable | Description | Default |
| --- | --- | --- |
| `MCP2TERM_SHELL` | Shell executable used for commands. | `/bin/bash` |
| `MCP2TERM_WORKDIR` | Working directory for commands. | Current directory |
| `MCP2TERM_INHERIT_ENV` | When `true`, inherit the parent environment. | `true` |
| `MCP2TERM_EXTRA_ENV` | JSON object merged into the command environment. | `{}` |
| `MCP2TERM_PLUGINS` | Comma-separated dotted module paths to load as plugins. | *(none)* |
| `MCP2TERM_COMMAND_TIMEOUT` | Default timeout in seconds for commands. | unlimited |
| `MCP2TERM_STREAM_CHUNK_SIZE` | Bytes read from stdout/stderr per chunk while streaming. | `65536` |
| `MCP2TERM_LONG_COMMAND_NOTICE_DELAY` | Seconds to wait before emitting long-running command notices. | `2.0` |
| `MCP2TERM_LONG_COMMAND_NOTICE_INTERVAL` | Interval in seconds between long-running command notices. | `5.0` |
| `MCP2TERM_CONSOLE_ECHO` | Mirror commands and output to the server console (`true`/`false`). | `true` |
| `MCP2TERM_CHAT_TERMINAL` | Set to `disabled` to suppress the console-integrated messaging bridge. Legacy values are accepted but ignored. | *(unused)* |
## Running the server
```bash
mcp2term --transport stdio
```
Change `--transport` to `sse` or `streamable-http` to use the corresponding MCP transports. `--log-level` controls verbosity and `--mount-path` overrides the HTTP mount location when relevant.
While the server is running it mirrors every executed command, stdout chunk, and stderr chunk to the hosting console. Set `MCP2TERM_CONSOLE_ECHO=false` to suppress the mirroring when embedding the server into log-sensitive environments.
When running with the `streamable-http` transport the MCP endpoint is served from the `/mcp` path (or `--mount-path` plus `/mcp` when a custom mount is provided). The CLI prints the fully qualified URL, including the `/mcp` suffix, to make tunnelling targets such as ngrok easy to copy.
## MCP tools
The server exposes two tools for remote command management:
`run_command(command: str, working_directory: Optional[str], environment: Optional[dict[str, str]], timeout: Optional[float]], command_id: Optional[str])`
The tool returns structured JSON containing:
- `command_id`: unique identifier assigned to the invocation
- `command`: executed command string
- `working_directory`: resolved working directory
- `return_code`: process exit code (non-zero for failure)
- `stdout` / `stderr`: aggregated output
- `started_at` / `finished_at`: ISO 8601 timestamps
- `duration`: execution duration in seconds
- `timed_out`: boolean flag indicating whether a timeout occurred
While a command runs the server emits stdout and stderr chunks as MCP log messages, preserving ordering through asynchronous streaming. Clients can reuse `command_id` values when making follow-up requests.
`cancel_command(command_id: str, signal_value: Optional[str | int])`
Sending `cancel_command` forwards a signal (defaulting to `SIGINT`) to the running process identified by `command_id`. The response includes the numeric `signal`, its symbolic `signal_name`, and a `delivered` flag confirming whether the process was still active when the signal was sent.
`send_stdin(command_id: str, data: Optional[str], eof: bool = False)`
Use `send_stdin` to stream additional input to an interactive command. The tool accepts optional text payloads and an `eof` flag
that closes the stdin pipe once all required data has been delivered. The response reports whether the input was accepted so
clients can retry or surface helpful diagnostics.
`manage_file(path: str, *, operation: str, content: Optional[str] = None, pattern: Optional[str] = None, line: Optional[int] = None, start_line: Optional[int] = None, end_line: Optional[int] = None, encoding: str = "utf-8", create_parents: bool = False, overwrite: bool = False, create_if_missing: bool = True, escape_profile: str = "auto", follow_symlinks: bool = True, use_regex: bool = False, ignore_case: bool = False, max_replacements: Optional[int] = None, anchor: Optional[str] = None, anchor_use_regex: bool = False, anchor_ignore_case: bool = False, anchor_after: bool = False, anchor_occurrence: Optional[int] = None)`
`manage_file` powers the `filetool` client command and exposes a broad suite of line-aware editing operations. The `escape_profile`
parameter controls how inline `--content` payloads are normalised before they reach the server:
- `auto` (default) mirrors the original behaviour and expands `\n`, `\t`, `\r`, and `\0` sequences when the payload would otherwise be a single line.
- `none` disables all inline decoding so payloads arrive exactly as typed, perfect for binary-friendly workflows or when backslashes carry semantic meaning.
- Additional profiles can be registered by extensions to enforce organisation-specific escaping rules. The selected profile is forwarded to plugins via the `FileOperationEvent` payload so observability tooling can respond appropriately.
Recent updates add top-of-file editing and pattern-driven substitutions to the toolbox:
- `prepend` injects content at the start of a file and respects `--create-if-missing` so you can bootstrap brand new files with headers in a single command.
- `insert` now accepts literal or regex anchors via `--anchor`, `--anchor-after`, `--anchor-ignore-case`, and `--anchor-occurrence`, making it easy to land changes relative to sentinel text without counting lines.
- `substitute --pattern PATTERN --content TEXT` performs literal or regex-based replacements while streaming structured metadata (matched pattern, replacement counts, and flags such as `--ignore-case` or `--max-replacements`) back to the caller.
Example usages:
```bash
# Create a multi-line file from a single-shell command using the default profile.
filetool write docs/roadmap.txt --content 'phase-one\\nphase-two\\nphase-three'
# Append literal escape sequences without rewriting them by selecting the "none" profile.
filetool append docs/roadmap.txt --content 'literal\\nvalue' --escape-profile none
# Use stdin for bulk updates while still labelling the request for plugins.
cat release.diff | filetool patch docs/roadmap.txt --stdin --escape-profile auto
```
## Plugins
Plugins implement the `PluginProtocol` (via a module-level `PLUGIN` object) and can register `CommandStreamListener` instances to observe command lifecycle events. When the server starts it loads modules listed in `MCP2TERM_PLUGINS`, exposing the entire `mcp2term` namespace through the plugin registry for inspection or extension.
A minimal plugin skeleton:
```python
from dataclasses import dataclass
from mcp2term.plugin import CommandStreamListener, PluginProtocol, PluginRegistry
@dataclass
class EchoListener(CommandStreamListener):
async def on_command_stdout(self, event):
print(event.data, end="")
async def on_command_start(self, event):
print(f"Starting: {event.request.command}")
async def on_command_stderr(self, event):
print(f"[stderr] {event.data}", end="")
async def on_command_complete(self, event):
print(f"Finished with {event.return_code}")
class ShellEchoPlugin(PluginProtocol):
name = "shell-echo"
version = "1.0.0"
def activate(self, registry: PluginRegistry):
registry.register_command_listener(EchoListener())
class AuditListener:
async def on_file_operation(self, event):
print(f"{event.operation} {event.path}: {event.result.message}")
registry.register_file_operation_listener(AuditListener())
PLUGIN = ShellEchoPlugin()
```
Listeners registered through `register_file_operation_listener` receive `FileOperationEvent`
instances containing the original request arguments, the resolved path, the
`FileOperationResult`, and any warning emitted during processing. This makes it
straightforward to build auditing, notification, or synchronization plugins that
react to remote edits in real time without modifying the core server.
## Development
Run the test suite with:
```bash
pytest
```
Tests are parameterised to run with or without dependency stubbing, ensuring full execution paths remain verified.
## Ngrok integration
By default `mcp2term` opens an ngrok tunnel whenever you run the server with the `sse` or `streamable-http` transports. The tunnel exposes the local HTTP endpoint using the ngrok agent that must already be authenticated (for example via `ngrok config add-authtoken`). Unless overridden, the server now requests the reserved domain `alpaca-model-easily.ngrok-free.app` so clients always receive a predictable hostname.
Control the integration with the following environment variables:
| Variable | Description | Default |
| --- | --- | --- |
| `MCP2TERM_NGROK_ENABLE` | Enable or disable automatic tunnel creation. | `true` |
| `MCP2TERM_NGROK_TRANSPORTS` | Comma-separated transports that should be tunnelled (`stdio`, `sse`, `streamable-http`). | `sse,streamable-http` |
| `MCP2TERM_NGROK_BIN` | Path to the `ngrok` executable. | `ngrok` |
| `MCP2TERM_NGROK_API_URL` | Base URL for the local ngrok API. | `http://127.0.0.1:4040` |
| `MCP2TERM_NGROK_REGION` | Optional ngrok region to target. | *(none)* |
| `MCP2TERM_NGROK_LOG_LEVEL` | ngrok log level (`debug`, `info`, `warn`, `error`). | `info` |
| `MCP2TERM_NGROK_EXTRA_ARGS` | JSON array of additional CLI arguments passed to ngrok. | `[]` |
| `MCP2TERM_NGROK_ENV` | JSON object merged into the ngrok process environment. | `{}` |
| `MCP2TERM_NGROK_START_TIMEOUT` | Seconds to wait for tunnel provisioning. | `15` |
| `MCP2TERM_NGROK_POLL_INTERVAL` | Seconds between tunnel status checks. | `0.5` |
| `MCP2TERM_NGROK_REQUEST_TIMEOUT` | HTTP timeout for API calls. | `5` |
| `MCP2TERM_NGROK_SHUTDOWN_TIMEOUT` | Seconds to wait for ngrok to terminate gracefully. | `5` |
| `MCP2TERM_NGROK_CONFIG` | Optional path to an ngrok configuration file. | *(none)* |
| `MCP2TERM_NGROK_HOSTNAME` / `MCP2TERM_NGROK_DOMAIN` / `MCP2TERM_NGROK_EDGE` | Custom host bindings to request from ngrok. | `alpaca-model-easily.ngrok-free.app` for domain |
Use the `--disable-ngrok` flag when running `mcp2term` to opt out of tunneling for a single invocation.
The configuration also records the directory where the server process was
launched and exports it to ``PYTHONPATH``. This mirrors running
``export PYTHONPATH=$(pwd)`` before starting the server so that any Python code
executed via ``run_command`` inherits the same module search path even when the
working directory is overridden.