Skip to main content
Glama
README.md
# 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.

TDQS

B3.2/5.0

Scored across 4 tools

Disambiguation5/5

Each tool has a clearly distinct purpose with no overlap: cancel_command stops processes, manage_file handles file operations, run_command executes commands, and send_stdin provides input to running commands. The boundaries are well-defined and unambiguous.

Naming Consistency4/5

Three tools follow a consistent verb_noun pattern (cancel_command, run_command, send_stdin), but manage_file uses a more general verb that slightly deviates from the others. The naming is still highly readable and mostly consistent.

Tool Count5/5

With 4 tools, this server is well-scoped for remote command and file management. Each tool earns its place by covering essential operations without bloat, making it appropriate for its purpose.

Completeness4/5

The toolset covers core remote execution workflows: running commands, managing files, and interacting with processes. A minor gap is the lack of a tool for listing or monitoring running processes, but agents can work around this using run_command with appropriate shell commands.

Maintenance

ActivityInactive
ResponsivenessNo issues