Metadata-Version: 2.4
Name: algony-mymcp
Version: 2.0.2
Summary: Linux system control MCP server over Streamable HTTP with Bearer token auth
Author-email: algony-tony <txzhu1010@gmail.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/algony-tony/mymcp
Project-URL: Repository, https://github.com/algony-tony/mymcp
Project-URL: Issues, https://github.com/algony-tony/mymcp/issues
Project-URL: Changelog, https://github.com/algony-tony/mymcp/blob/master/CHANGELOG.md
Keywords: mcp,model-context-protocol,linux,remote-control,fastapi
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: POSIX :: Linux
Classifier: Topic :: System :: Systems Administration
Classifier: Framework :: FastAPI
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp>=1.0.0
Requires-Dist: fastapi>=0.115.0
Requires-Dist: uvicorn[standard]>=0.30.0
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: httpx>=0.27.0
Requires-Dist: anyio>=4.0.0
Requires-Dist: prometheus-client>=0.20.0
Requires-Dist: pydantic-settings>=2.0
Requires-Dist: python-json-logger>=2.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-anyio; extra == "dev"
Requires-Dist: pytest-benchmark; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: mypy>=1.11; extra == "dev"
Requires-Dist: pre-commit>=3.7; extra == "dev"
Requires-Dist: mutmut; extra == "dev"
Requires-Dist: build; extra == "dev"
Dynamic: license-file

# Linux MCP Server

[![CI](https://github.com/algony-tony/mymcp/actions/workflows/ci.yml/badge.svg)](https://github.com/algony-tony/mymcp/actions/workflows/ci.yml)
[![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/algony-tony/f5b7d1a23781d63db40ea2e2dcdf71c2/raw/mymcp-coverage.json&cacheSeconds=3600)](https://github.com/algony-tony/mymcp/actions/workflows/ci.yml)
[![Branch Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/algony-tony/f5b7d1a23781d63db40ea2e2dcdf71c2/raw/mymcp-branch-coverage.json&cacheSeconds=3600)](https://github.com/algony-tony/mymcp/actions/workflows/ci.yml)
[![Mutation Score](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/algony-tony/f5b7d1a23781d63db40ea2e2dcdf71c2/raw/mymcp-mutation.json&cacheSeconds=3600)](https://github.com/algony-tony/mymcp/actions/workflows/ci.yml)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)

A Python MCP server that exposes full Linux system control to AI clients (Claude Desktop, Claude Code, Cursor, Gemini CLI, etc.) over Streamable HTTP.

## Features

- **6 MCP tools**: `bash_execute`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`
- **Per-token permissions**: read-only (`ro`) or read-write (`rw`) roles
- **Audit logging**: JSON Lines audit trail with error details for all tool invocations
- **Application logging**: errors and warnings output to stderr (captured by journald)
- **Protected paths**: MCP's own files are protected from tool access
- **Multi-user auth**: Bearer token authentication with per-token management
- **Admin API**: create/revoke tokens without restarting the server
- **Streamable HTTP transport**: stateless mode, each request is independent (no session issues on reconnect)

## Requirements

- Python 3.11+
- `ripgrep` (`rg`) — optional but recommended for faster `grep` tool (falls back to Python regex)

## Install

Requires Python 3.11+ on Linux.

```bash
pipx install algony-mymcp
```

The PyPI distribution name is `algony-mymcp` (the bare name `mymcp` is reserved
on PyPI). After install the command and the Python import path are still
plain `mymcp`.

Plain `pip` works too (a venv is recommended):

```bash
python3 -m venv ~/.local/share/mymcp-env
~/.local/share/mymcp-env/bin/pip install algony-mymcp
ln -s ~/.local/share/mymcp-env/bin/mymcp ~/.local/bin/mymcp
```

### Quick try (foreground, no system service)

```bash
mymcp serve
```

`mymcp` prints a temporary admin and rw token to stderr, listens on
`0.0.0.0:8765` by default, and discards both tokens on exit.

### Production install (systemd)

```bash
sudo mymcp install-service --yes
sudo systemctl start mymcp
```

This writes `/etc/mymcp/.env`, generates an admin token (printed once),
optionally generates a metrics token, installs `/etc/systemd/system/mymcp.service`,
sets up logrotate for `/var/log/mymcp/audit.log`, and (by default) installs
`ripgrep` for fast file search.

Useful flags: `--port 9000`, `--bind 127.0.0.1`, `--config-dir`, `--log-dir`,
`--service-user mymcp` (run as a restricted user), `--no-metrics`,
`--no-audit`, `--skip-ripgrep`.

### Upgrade

```bash
pipx upgrade algony-mymcp
sudo systemctl restart mymcp
```

### Air-gapped install

Each GitHub Release ships a `mymcp-X.Y.Z-offline-bundle.tar.gz` containing
all wheels and ripgrep binaries:

```bash
tar xzf mymcp-2.0.0-offline-bundle.tar.gz
cd mymcp-2.0.0-offline-bundle
sudo ./install-offline.sh
sudo mymcp install-service --yes
```

## CLI Reference

### Top-level commands

| Command | Purpose |
|---------|---------|
| `mymcp serve` | Run the MCP server in the foreground |
| `mymcp version` | Print the installed version |
| `mymcp install-service` | Install the systemd service and config files |
| `mymcp uninstall-service` | Remove the systemd service |
| `mymcp token ...` | Manage tokens in the local token store |
| `mymcp migrate-from-legacy` | Migrate a 1.x `/opt/mymcp` install to 2.x |
| `mymcp doctor` | Print environment and dependency diagnostics |

### `mymcp serve`

```bash
mymcp serve --help
```

Important flags:

| Flag | Description |
|------|-------------|
| `--env-file PATH` | Load settings from a specific env file |
| `--host HOST` | Override bind host |
| `--port PORT` | Override bind port |
| `--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}` | Set application log level |
| `--log-format {text,json}` | Use text or JSON stderr logs |
| `--with-metrics-token` | In temporary-token mode, also generate an ephemeral metrics token |

### `mymcp install-service`

```bash
sudo mymcp install-service --help
```

Important flags:

| Flag | Description |
|------|-------------|
| `--port PORT` | Listen port (default `8765`) |
| `--bind HOST` | Bind address (default `0.0.0.0`) |
| `--config-dir PATH` | Config directory (default `/etc/mymcp`) |
| `--log-dir PATH` | Audit log directory (default `/var/log/mymcp`) |
| `--service-user {root,mymcp}` | Run as root or a restricted `mymcp` user |
| `--enable-metrics` / `--no-metrics` | Enable or disable `/metrics` token setup |
| `--enable-audit` / `--no-audit` | Enable or disable audit logging setup |
| `--install-ripgrep` / `--skip-ripgrep` | Install or skip `rg` for fast grep |
| `--yes` | Skip interactive confirmation |

### `mymcp uninstall-service`

```bash
sudo mymcp uninstall-service --help
```

| Flag | Description |
|------|-------------|
| `--config-dir PATH` | Config directory to target |
| `--log-dir PATH` | Log directory to target |
| `--purge` | Also delete config and log directories |

### `mymcp token`

```bash
mymcp token --help
```

Subcommands:

| Subcommand | Purpose |
|------------|---------|
| `mymcp token list` | Show admin/metrics state and ro/rw tokens |
| `mymcp token add --name NAME --role {ro,rw}` | Create a new token |
| `mymcp token revoke TOKEN` | Delete a token |
| `mymcp token rotate-admin` | Generate and persist a new admin token |
| `mymcp token rotate-metrics` | Generate and persist a new metrics token |
| `mymcp token disable-metrics` | Disable the `/metrics` endpoint |

### `mymcp migrate-from-legacy`

```bash
sudo mymcp migrate-from-legacy --help
```

| Flag | Description |
|------|-------------|
| `--legacy-dir PATH` | Legacy 1.x install root (default `/opt/mymcp`) |
| `--dry-run` | Show planned migration steps without changing files |

### `mymcp doctor`

Use this when install, Python path, `rg`, or env-file resolution looks wrong:

```bash
mymcp doctor
```

## Upgrading from 1.x to 2.0

Breaking changes:
- Environment variable prefix renamed: `MCP_*` → `MYMCP_*` (no compat shim).
- Install layout: `/opt/mymcp/` (1.x) → `/etc/mymcp/` (2.0). Code is now
  managed by `pipx`, not unpacked into `/opt/mymcp/`.
- Install method: `git clone + deploy/install.sh` → `pipx install algony-mymcp`.

One-line migration:

```bash
pipx install algony-mymcp
sudo mymcp migrate-from-legacy
sudo rm -rf /opt/mymcp     # after verifying the new service is healthy
```

`mymcp migrate-from-legacy` reads `/opt/mymcp/.env`, rewrites `MCP_*` keys to
`MYMCP_*`, copies `tokens.json`, installs the new systemd unit, and restarts
the service. Pass `--dry-run` to see what it would do without making changes.

The legacy `deploy/install.sh` and `deploy/upgrade.sh` scripts remain in the
repository through the 2.0.x lifecycle for users who can't migrate yet.

## Configuration

`mymcp install-service` writes `/etc/mymcp/.env`. The `serve` command also
honors `--env-file PATH`, `MYMCP_ENV_FILE`, and (in dev) `./.env`.

### Core

| Variable | Default | Description |
|----------|---------|-------------|
| `MYMCP_ADMIN_TOKEN` | *(required for /admin)* | Admin token for managing user tokens |
| `MYMCP_METRICS_TOKEN` | *(empty = disabled)* | Bearer for `/metrics` endpoint |
| `MYMCP_HOST` | `0.0.0.0` | Bind address |
| `MYMCP_PORT` | `8765` | Listen port |
| `MYMCP_TOKEN_FILE` | `/etc/mymcp/tokens.json` | Token store path |
| `MYMCP_PROTECTED_PATHS` | *(empty)* | Additional protected paths, comma-separated |
| `MYMCP_SHUTDOWN_GRACE_SEC` | `5` | Seconds to wait for in-flight bash children on SIGTERM |

### Audit Logging

| Variable | Default | Description |
|----------|---------|-------------|
| `MYMCP_AUDIT_ENABLED` | `false` | Enable audit logging |
| `MYMCP_AUDIT_LOG_DIR` | `/var/log/mymcp` | Audit log directory (auto-protected) |
| `MYMCP_AUDIT_MAX_BYTES` | `10485760` | Max audit log file size before rotation (10MB) |
| `MYMCP_AUDIT_BACKUP_COUNT` | `5` | Number of rotated log files to keep |

### Tool Limits

All limits are configurable via environment variables. Default values work well for most use cases.

| Variable | Default | Description |
|----------|---------|-------------|
| `MYMCP_BASH_MAX_OUTPUT_BYTES` | `102400` | bash stdout/stderr default cap (100KB) |
| `MYMCP_BASH_MAX_OUTPUT_BYTES_HARD` | `1048576` | bash output hard cap (1MB) |
| `MYMCP_READ_FILE_DEFAULT_LIMIT` | `2000` | read_file default lines per request |
| `MYMCP_READ_FILE_MAX_LIMIT` | `50000` | read_file max lines per request |
| `MYMCP_READ_FILE_MAX_LINE_BYTES` | `32768` | Max bytes per line before truncation (32KB) |
| `MYMCP_WRITE_FILE_MAX_BYTES` | `10485760` | write_file max size (10MB) |
| `MYMCP_EDIT_STRING_MAX_BYTES` | `1048576` | edit_file max old/new string size (1MB) |
| `MYMCP_GLOB_MAX_RESULTS` | `1000` | Max file paths returned by glob |
| `MYMCP_GREP_DEFAULT_MAX_RESULTS` | `500` | grep default max matches |
| `MYMCP_GREP_MAX_RESULTS` | `5000` | grep hard max matches |

## Managing Tokens

The `mymcp token` subcommands operate on the local token store directly (no
admin API call required). They read `/etc/mymcp/.env` by default; use
`MYMCP_ENV_FILE=...` to point elsewhere.

```bash
# List all tokens (admin/metrics state + ro/rw entries)
sudo mymcp token list

# Create a read-only token
sudo mymcp token add --name my-claude-desktop --role ro

# Create a read-write token
sudo mymcp token add --name my-admin-client --role rw

# Revoke
sudo mymcp token revoke tok_abc123

# Rotate the admin or metrics token (rewrites .env)
sudo mymcp token rotate-admin
sudo mymcp token rotate-metrics

# Disable the /metrics endpoint by emptying the metrics token
sudo mymcp token disable-metrics
```

The HTTP `/admin/*` API still works for clients that need to manage tokens
remotely; it requires `Authorization: Bearer <MYMCP_ADMIN_TOKEN>`.

## HTTP Endpoints

### Public / operational endpoints

| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| `GET` | `/health` | none | Liveness check with version |
| `GET` | `/version` | none | Return just the server version |
| `GET` | `/metrics` | metrics token | Prometheus metrics |
| `POST` | `/mcp` | ro/rw token | Streamable HTTP MCP transport |

Examples:

```bash
curl http://your-server:8765/health
curl http://your-server:8765/version
curl -H "Authorization: Bearer $MYMCP_METRICS_TOKEN" \
  http://your-server:8765/metrics
```

Notes:
- `/metrics` returns `401` if the bearer token is wrong.
- `/metrics` returns `503` if metrics support is disabled or no metrics token is configured.

### Admin API

All `/admin/*` endpoints require:

```http
Authorization: Bearer <MYMCP_ADMIN_TOKEN>
```

| Method | Path | Body | Purpose |
|--------|------|------|---------|
| `GET` | `/admin/tokens` | none | List managed ro/rw tokens |
| `POST` | `/admin/tokens` | `{"name":"...", "role":"ro|rw"}` | Create a token |
| `DELETE` | `/admin/tokens/{token}` | none | Revoke a token |

Examples:

```bash
# List tokens
curl -H "Authorization: Bearer $MYMCP_ADMIN_TOKEN" \
  http://your-server:8765/admin/tokens

# Create a read-only token
curl -X POST \
  -H "Authorization: Bearer $MYMCP_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"ci-bot","role":"ro"}' \
  http://your-server:8765/admin/tokens

# Revoke a token
curl -X DELETE \
  -H "Authorization: Bearer $MYMCP_ADMIN_TOKEN" \
  http://your-server:8765/admin/tokens/tok_abc123
```

## Connecting Clients

### Claude Desktop / Cursor

Add to MCP settings:

```json
{
  "mcpServers": {
    "linux-server": {
      "type": "streamableHttp",
      "url": "http://your-server:8765/mcp",
      "headers": {
        "Authorization": "Bearer tok_abc123"
      }
    }
  }
}
```

### Claude Code

```bash
claude mcp add linux-server \
  --transport streamable-http \
  --url http://your-server:8765/mcp \
  --header "Authorization: Bearer tok_abc123"
```

## MCP Tools

### Permission model

| Tool | Permission | Summary |
|------|-----------|---------|
| `bash_execute` | rw | Run a shell command in a fresh subprocess |
| `read_file` | ro | Read a file with line numbers and pagination |
| `write_file` | rw | Create or overwrite a file |
| `edit_file` | rw | Replace text in a file |
| `glob` | ro | Find paths by glob pattern |
| `grep` | ro | Search file contents with regex |

`ro` tokens can only use `read_file`, `glob`, and `grep`.  
`rw` tokens can use all six tools.

### `bash_execute` (`rw`)

Runs a shell command in a new subprocess. Commands are stateless: one call does
not preserve cwd, environment changes, shell functions, or exports for the next.

Parameters:

| Field | Type | Required | Default | Notes |
|------|------|----------|---------|-------|
| `command` | string | yes | – | Shell command to run |
| `timeout` | integer | no | `30` | Clamped to `1..600` seconds |
| `working_dir` | string | no | `/` | Command cwd |
| `max_output_bytes` | integer | no | `102400` | Per-stream cap; hard max `1048576` |

Returns:
- `stdout`
- `stderr`
- `exit_code`
- `timed_out`

Example:

```json
{
  "command": "uname -a",
  "timeout": 10,
  "working_dir": "/tmp"
}
```

### `read_file` (`ro`)

Reads a file and returns numbered lines. Large reads support pagination.

Parameters:

| Field | Type | Required | Default | Notes |
|------|------|----------|---------|-------|
| `file_path` | string | yes | – | Absolute path |
| `offset` | integer | no | `1` | 1-based start line |
| `limit` | integer | no | `2000` | Runtime max `50000` |

Returns:
- `content`
- `total_lines`
- `truncated`

Common errors:
- `FileNotFoundError`
- `IsADirectoryError`
- `PermissionError`
- `ProtectedPath`

Example:

```json
{
  "file_path": "/var/log/syslog",
  "offset": 1,
  "limit": 200
}
```

### `write_file` (`rw`)

Creates or overwrites a file in one shot.

Parameters:

| Field | Type | Required | Default | Notes |
|------|------|----------|---------|-------|
| `file_path` | string | yes | – | Absolute path |
| `content` | string | yes | – | Max size `10485760` bytes (10 MB) |

Notes:
- Missing parent directories are created automatically.
- Protected paths are rejected.

Example:

```json
{
  "file_path": "/tmp/hello.txt",
  "content": "hello from mymcp\n"
}
```

### `edit_file` (`rw`)

Replaces text in an existing file.

Parameters:

| Field | Type | Required | Default | Notes |
|------|------|----------|---------|-------|
| `file_path` | string | yes | – | Absolute path |
| `old_string` | string | yes | – | Max size `1048576` bytes |
| `new_string` | string | yes | – | Max size `1048576` bytes |
| `replace_all` | boolean | no | `false` | If false, `old_string` must match uniquely |

Use this when the client wants a precise in-file replacement instead of full overwrite.

Example:

```json
{
  "file_path": "/tmp/app.conf",
  "old_string": "PORT=8000",
  "new_string": "PORT=8765"
}
```

### `glob` (`ro`)

Finds matching files under a root directory. Results are sorted by modified time descending.

Parameters:

| Field | Type | Required | Default | Notes |
|------|------|----------|---------|-------|
| `pattern` | string | yes | – | Example: `**/*.py` |
| `path` | string | no | `/` | Root directory |

Runtime max results: `1000`.

Example:

```json
{
  "pattern": "**/*.log",
  "path": "/var/log"
}
```

### `grep` (`ro`)

Searches file contents with a regex. Uses `ripgrep` if available; otherwise falls back to a Python implementation.

Parameters:

| Field | Type | Required | Default | Notes |
|------|------|----------|---------|-------|
| `pattern` | string | yes | – | Regex pattern |
| `path` | string | no | `/` | File or directory to search |
| `glob` | string | no | none | Filename filter, e.g. `*.py` |
| `output_mode` | string | no | `content` | One of `content`, `files`, `count` |
| `context_lines` | integer | no | `0` | Include surrounding lines |
| `max_results` | integer | no | `500` | Runtime max `5000` |
| `case_insensitive` | boolean | no | `false` | Case-insensitive search |

Example:

```json
{
  "pattern": "Authorization",
  "path": "/etc",
  "glob": "*.conf",
  "output_mode": "content",
  "context_lines": 2
}
```

### Tool behavior notes

- File tools enforce protected-path checks.
- `bash_execute` does **not** enforce protected-path checks; use `ro` tokens for untrusted clients.
- `grep` and `glob` are capped to protect the server from unbounded scans.
- `read_file` truncates oversized individual lines and marks them with `[LINE TRUNCATED]`.

## Logging

### Audit Log

When enabled (`MYMCP_AUDIT_ENABLED=true`), all tool invocations are logged to `<MYMCP_AUDIT_LOG_DIR>/audit.log` in JSON Lines format:

```json
{"ts":"2026-04-10T15:30:22Z","token_name":"my-client","role":"rw","ip":"203.0.113.5","tool":"bash_execute","params":{"command":"apt update"},"result":"ok","duration_ms":1523}
```

Error entries include `error_code` and `error_message`:

```json
{"ts":"2026-04-10T15:31:00Z","token_name":"ro-client","role":"ro","ip":"203.0.113.5","tool":"read_file","params":{"file_path":"/var/log/mymcp/audit.log"},"result":"error","error_code":"ProtectedPath","error_message":"Access denied: path is within protected directory","duration_ms":0}
```

Logs rotate automatically (default 10MB with 5 backups).

### Application Log

Tool errors and warnings are also output to stderr, which is captured by journald when running as a systemd service:

```bash
journalctl -u mymcp -f
```

## Protected Paths

MCP automatically protects its own installation directory and audit log directory from access via file tools (`read_file`, `write_file`, `edit_file`, `glob`, `grep`). This prevents AI clients from reading tokens, modifying server code, or tampering with audit logs.

Add extra protected paths via `MYMCP_PROTECTED_PATHS=/path/one,/path/two`.

Note: `bash_execute` is not subject to path protection — use `ro` tokens for untrusted clients.

## Monitoring

mymcp provides a Prometheus-compatible `/metrics` endpoint.

### Configuration
1. **Metrics Token**: Set the `MYMCP_METRICS_TOKEN` environment variable to secure the endpoint.
2. **Prometheus Scrape**: Configure Prometheus to scrape `/metrics` using the bearer token.
   ```yaml
   scrape_configs:
     - job_name: mymcp
       metrics_path: /metrics
       authorization:
         type: Bearer
         credentials: <MYMCP_METRICS_TOKEN>
       static_configs:
         - targets: ["your-host:8765"]
   ```

### Dashboard
A pre-built Grafana dashboard is available in `deploy/grafana/mymcp-dashboard.json`. It provides visualization for:
- Tool call rates and error status.
- p50/p95/p99 latency per tool.
- HTTP request statistics.
- System health (CPU, Memory, FDs).

See [deploy/grafana/README.md](deploy/grafana/README.md) for detailed import and setup instructions.

## Testing

```bash
# Run all tests (excludes benchmarks)
python -m pytest tests/ -v --benchmark-disable

# Run with coverage report
python -m pytest tests/ -v --cov=. --cov-branch --cov-report=term-missing --benchmark-disable

# Run benchmark tests only
python -m pytest tests/test_benchmark.py --benchmark-only -v

# Save benchmark baseline for comparison
python -m pytest tests/test_benchmark.py --benchmark-save=baseline

# Run mutation testing
python -m mutmut run --max-children 1
python -m mutmut results

# Run load tests (start server first: mymcp serve)
export MYMCP_TEST_TOKEN=<your-rw-token>
locust -f tests/loadtest/locustfile.py --host http://localhost:8765
```

### Test Dimensions

| Dimension | Tool | Target |
|-----------|------|--------|
| Line coverage | pytest-cov | 97%+ |
| Branch coverage | pytest-cov --cov-branch | tracked |
| Integration tests | httpx ASGITransport | full auth->tool->audit chain |
| Boundary analysis | pytest | all parameter edge cases |
| Performance benchmarks | pytest-benchmark | per-function timing |
| Load testing | locust | multi-user concurrency |
| Mutation testing | mutmut | 80%+ score |

## Security Note

This server grants system access to AI clients. Security measures:

- **Permissions**: New tokens default to `ro` (read-only). Only grant `rw` to trusted clients.
- **Audit**: Enable audit logging to track all tool invocations.
- **Protected paths**: Server files are automatically protected from tool access.
- **Network**: Run behind a firewall and consider TLS (e.g. via nginx reverse proxy).
