Skip to main content
Glama
Gus2708
by Gus2708

sftp-manager

Manage your game servers by talking to them. An MCP server that gives any AI agent safe, sandboxed SFTP access to your Minecraft / Pterodactyl / TaroHosting boxes — read configs, edit them surgically, install addons, restore backups. No shell required.

tests License: MIT Python 3.10+ MCP agent-installable private repo

Private repository. Access is by invitation — see Access. The CI badge is static because shields.io cannot read a private repo's workflow status; the live run is under the Actions tab.

you> on the survival server, bump max players to 40

ai>  Reading the file first.
     -> leer_archivo({"ruta": "server.properties"})
     -> reemplazar_en_archivo({"ruta": "server.properties",
                               "buscar": "max-players=20",
                               "reemplazar": "max-players=40"})

     [!] This operation modifies the server. Approve? (y/N)

ai>  Done. max-players is now 40.
     Backup saved at .ai_backups/20260731-004512__server.properties
     Restart the server from your panel to apply it.

Why this exists

Game hosting panels (Pterodactyl, TaroHosting, and friends) expose SFTP only — no shell. No ssh, no unzip, no grep, no rm -rf. Editing one line of a config becomes a download / edit / re-upload round trip through a clunky web file manager, and installing a mod pack is a manual unzip-and-drag ritual across hundreds of files.

sftp-manager hands that whole workflow to an AI agent through the Model Context Protocol — with guardrails, so the agent can't wander outside your server root, and with a parallel transfer engine, so operations that take minutes over a naive SFTP client take seconds.

Related MCP server: mcp-remote-ssh

Features

  • Self-installing — hand the folder to any AI agent and say "read AGENTS.md and install this as an MCP server". install_mcp.py diagnoses the environment, writes the config for nine MCP clients, and proves the result with a real MCP handshake. See Install.

  • 20 MCP tools covering the full lifecycle: browse, read, search, edit, upload, install, delete, restore — plus start / stop / restart through the panel API.

  • Multi-server registry — declare every box you own in one servers.json, switch with a servidor parameter.

  • Host key verification — trust-on-first-use by default, strict when you want it.

  • Parallel transfer engine — bulk uploads and recursive deletes are spread across multiple SSH connections. See How it works.

  • Path sandbox — every path resolves against the configured root before any network call.

  • Automatic backups — every overwrite and delete copies the original into .ai_backups/ on the server first, with a timestamp. Fully restorable.

  • Surgical editsreemplazar_en_archivo refuses to run when the search fragment is ambiguous, instead of guessing.

  • Destructive-operation hints — write and delete tools carry MCP destructiveHint, so your client asks before executing and shows the exact arguments.

  • Self-healing connections — keepalive packets plus transparent reconnect when the transport dies.

  • Per-tool timeouts — no call hangs forever; on timeout the connection is recycled and a clear error is returned.

  • Secrets stay out of config — passwords are written as ${ENV_VAR} and resolved from the environment at load time.

  • Addon installer.zip / .mcaddon archives are extracted locally and the resulting tree uploaded, working around the missing remote shell. Includes Zip-Slip protection.

  • Bedrock world settings — reads and edits level.dat (little-endian NBT), so gamerules and experiment toggles like Beta APIs can be flipped without opening the game. See World settings.

Requirements

  • Python 3.10 or newer

  • An SFTP-capable host (Pterodactyl-based panels, TaroHosting, or any plain SFTP server)

Access

This repository is private. Cloning needs an authenticated GitHub account that has been added as a collaborator. Pick whichever you already have set up:

# GitHub CLI — handles auth for you
gh repo clone Gus2708/sftp-manager

# SSH key
git clone git@github.com:Gus2708/sftp-manager.git

# HTTPS — asks for a personal access token with `repo` scope, not your password
git clone https://github.com/Gus2708/sftp-manager.git

Being private changes nothing about how the server runs: it is launched locally over stdio and never phones home.

Install

Hand it to an AI agent

This repo is built to install itself. Clone it, point your agent at the folder and say:

Install this repo as an MCP server. Read AGENTS.md and follow it.

AGENTS.md is the agent playbook — the AGENTS.md convention is read automatically by Codex, Cursor, Copilot, Jules and Zed, and CLAUDE.md points Claude Code at the same file. It tells the agent exactly which commands to run, in what order, what to ask you for, and how to prove the result works.

The agent drives install_mcp.py, which is the actual install surface. It uses only the standard library — so it runs before anything is installed — and every subcommand emits JSON, so an agent branches on fields instead of guessing at prose:

Command

What it does

python install_mcp.py check

Python version, missing deps, config state, and a siguiente_paso telling you what to run next

python install_mcp.py deps

Installs requirements.txt with the right interpreter

python install_mcp.py init

Creates servers.json and .env from the examples, without overwriting

python install_mcp.py clients

Lists every MCP client on this machine, its config path, and whether it's already set up

python install_mcp.py install <client>

Merges the config into that client's file, after a timestamped backup

python install_mcp.py config <client>

Prints the block instead of writing it

python install_mcp.py verify

Boots the server, does a real MCP handshake, lists the 20 tools

python install_mcp.py doctor

check + verify in human-readable form

Two details make this reliable rather than merely convenient:

  • verify is a genuine MCP handshake, not a smoke test. It spawns mcp_server.py, sends initialize and tools/list over stdio and reads the replies. It opens no SFTP connection, so it works before you have credentials — proving the install is sound separately from proving the credentials are.

  • The generated config carries the absolute interpreter path (sys.executable), never a bare python. MCP clients don't inherit your shell's PATH, and that single detail is the most common reason a server silently fails to start.

Do it yourself

pip install -r requirements.txt
python install_mcp.py init          # or: cp servers.example.json servers.json
python install_mcp.py doctor

Only paramiko, python-dotenv and mcp are needed for the MCP server. nbtlib unlocks the two level.dat tools. anthropic is required exclusively for the optional standalone CLI.

If your environment is externally managed (PEP 668), use a virtualenv and keep using its interpreter for every later step — including install_mcp.py, so the config it writes points at the right Python:

python -m venv .venv
.venv/bin/python install_mcp.py deps      # Windows: .venv\Scripts\python.exe
.venv/bin/python install_mcp.py doctor

Configure your servers

servers.json declares every server you manage. Passwords are written as ${VARIABLE} references resolved from the environment (or a .env file), so the config file itself never holds a secret and is safe to commit or share.

{
  "default": "survival",
  "servers": {
    "survival": {
      "descripcion": "Main survival server",
      "juego": "Minecraft Bedrock 26.30",
      "host": "node1.tarohosting.net",
      "port": 2022,
      "user": "abc12345.f00d",
      "password": "${TARO_SURVIVAL_PASSWORD}",
      "root": "/",
      "max_read_bytes": 200000
    },
    "creative": {
      "descripcion": "Testing and builds",
      "host": "node2.tarohosting.net",
      "port": 2022,
      "user": "abc12345.beef",
      "password": "${TARO_CREATIVE_PASSWORD}",
      "root": "/"
    }
  }
}

Then put the actual secrets in .env (git-ignored):

TARO_SURVIVAL_PASSWORD=your-panel-password
TARO_CREATIVE_PASSWORD=your-other-password

Every tool accepts an optional servidor parameter. Omit it and the one marked default is used.

Keys accepted per server

Connection and identity:

Key

Required

Default

Notes

host

yes

Node address

user

yes

Alias: username

password

one of the two

Use ${ENV_VAR}

key_path

one of the two

SSH key instead of a password

root

no

/

Sandbox root — nothing outside is reachable

descripcion

no

""

Shown by listar_servidores. Alias: description

juego

no

""

Free text. Alias: game

Tuning — all optional, all with sane defaults. Reach for these when a node is slow, far away, or strict about concurrent connections:

Key

Env equivalent

Default

Purpose

port

SFTP_PORT

2022

Panels rarely use 22

max_read_bytes

SFTP_MAX_READ_BYTES

200000

Per-file read cap

canales

SFTP_CANALES

8

Parallel SSH connections for bulk operations

op_timeout

SFTP_OP_TIMEOUT

120

Seconds per operation once connected

umbral_paralelo

SFTP_UMBRAL_PARALELO

25

Item count above which extra connections are opened. 0 = always

keepalive

SFTP_KEEPALIVE

15

Seconds between keepalive packets

timeout

SFTP_TIMEOUT

20

Initial handshake timeout

backup_dir

SFTP_BACKUP_DIR

.ai_backups

Remote folder holding automatic backups

host_key_policy

SFTP_HOST_KEY_POLICY

accept-new

strict, accept-new or auto — see Host key verification

known_hosts

SFTP_KNOWN_HOSTS

~/.ssh/known_hosts

Where host keys are read from and written to

Optionally, a nested panel block enables power control (see Restarting the server):

"panel": {
  "url": "https://panel.tarohosting.net",
  "api_key": "${TARO_PANEL_API_KEY}",
  "server_id": "1a2b3c4d"
}

Every key is validated at load time: non-numeric or out-of-range values fail immediately with the server name and the offending key, rather than surfacing as a confusing network error on the first tool call. Unknown keys are rejected too, so a typo like canaless is caught at startup instead of being silently ignored.

For a Pterodactyl-style panel, the connection values live under Settings → SFTP Details. Note the username is usually panel-user.server-id, and the password is your panel password.

Single-server fallback

If servers.json doesn't exist, the server falls back to one connection read entirely from the environment, using the SFTP_* variable names in the tables above plus SFTP_HOST, SFTP_USER, SFTP_PASSWORD / SFTP_KEY_PATH and SFTP_ROOT. Defaults and validation are identical in both modes — there is exactly one source of truth for them, in SFTPConfig.

Point SFTP_SERVERS_FILE at another path to load the registry from somewhere else.


Registering it with your MCP client

The short version:

python install_mcp.py clients              # which ones are on this machine
python install_mcp.py install cursor       # merge it in, after a backup

Client id

Config file it writes

claude-code

.mcp.json in this repo (already shipped)

claude-desktop

claude_desktop_config.json, per-OS location

cursor

~/.cursor/mcp.json

vscode

.vscode/mcp.json

windsurf

~/.codeium/windsurf/mcp_config.json

zed

Zed settings.json

gemini

~/.gemini/settings.json

cline

Cline's cline_mcp_settings.json

codex

~/.codex/config.toml — printed, not written (TOML)

The file is merged rather than replaced, a timestamped .bak-* copy is made first, and an existing sftp-manager entry is left untouched unless you pass --force.

The rest of this section is the manual equivalent, for clients not on that list or when you would rather see what you're pasting. python install_mcp.py config <client> prints the same blocks with your real paths already filled in.

The server speaks stdio, the transport every MCP client supports. The command is always the same:

command: python
args:    ["/absolute/path/to/sftp-manager/mcp_server.py"]

On Windows, use double backslashes in JSON: "G:\\Projects\\sftp-manager\\mcp_server.py". If Python isn't on your PATH under that name, use python3 or the absolute path to your interpreter (/path/to/.venv/bin/python) — this is the single most common setup failure, and the reason install_mcp.py always writes an absolute path.

Most clients share the same mcpServers JSON shape, so this is the block to copy:

{
  "mcpServers": {
    "sftp-manager": {
      "command": "python",
      "args": ["/absolute/path/to/sftp-manager/mcp_server.py"]
    }
  }
}

Claude Code

The repo ships a project-scoped .mcp.json, so cloning and opening the folder is enough — approve the server when prompted and verify with /mcp. It also ships CLAUDE.md, so a fresh session already knows what the project is and how to install it.

That .mcp.json uses a bare python, which is portable but relies on your PATH. If the server fails to start, pin the interpreter:

python install_mcp.py install claude-code --force

To register it for every project instead of just this one:

claude mcp add sftp-manager --scope user -- /absolute/path/to/python /absolute/path/to/mcp_server.py

Claude Desktop

Paste the standard mcpServers block into claude_desktop_config.json:

  • macOS~/Library/Application Support/Claude/claude_desktop_config.json

  • Windows%APPDATA%\Claude\claude_desktop_config.json

  • Linux~/.config/Claude/claude_desktop_config.json

Restart the app afterwards.

Cursor

Standard mcpServers block in either:

  • ~/.cursor/mcp.json — available in every project

  • .cursor/mcp.json — this project only

Confirm under Settings → MCP.

VS Code (GitHub Copilot agent mode)

VS Code uses a servers key instead of mcpServers. Create .vscode/mcp.json:

{
  "servers": {
    "sftp-manager": {
      "type": "stdio",
      "command": "python",
      "args": ["/absolute/path/to/sftp-manager/mcp_server.py"]
    }
  }
}

Windsurf

Standard mcpServers block in ~/.codeium/windsurf/mcp_config.json.

Cline / Roo Code

Open the MCP Servers panel → Configure MCP Servers, then add the standard mcpServers block to the settings file it opens.

Zed

Zed calls them context servers. In settings.json:

{
  "context_servers": {
    "sftp-manager": {
      "source": "custom",
      "command": "python",
      "args": ["/absolute/path/to/sftp-manager/mcp_server.py"]
    }
  }
}

JetBrains IDEs (AI Assistant / Junie)

Settings → Tools → AI Assistant → Model Context Protocol (MCP) → Add, then either fill in the command and arguments, or paste the standard mcpServers JSON block.

OpenAI Codex CLI

~/.codex/config.toml uses TOML:

[mcp_servers.sftp-manager]
command = "python"
args = ["/absolute/path/to/sftp-manager/mcp_server.py"]

Gemini CLI

Standard mcpServers block in ~/.gemini/settings.json.

Anything else

Any MCP-compatible client works — point it at python mcp_server.py over stdio.

When it doesn't come up

Start here, because it answers the question without an editor in the way:

python install_mcp.py verify

"ok": true and 20 tools means the server is fine and the problem is in the client's config. Otherwise the stderr field carries the real reason.

Symptom

Cause

Fix

Client shows the server as failed, no logs

command is a bare python the client can't resolve

python install_mcp.py install <client> --force — it writes the absolute interpreter path

configuracion invalida on stderr

No servers.json and no .env

python install_mcp.py init, then fill them in

verify reports missing dependencies

Deps went into a different interpreter

Run install_mcp.py with the same Python listed in the client config

El servidor 'x' necesita 'password' o 'key_path'

A ${VAR} resolved to empty

The variable is missing from .env; check lists it under secretos_sin_resolver

Tool calls hang, then time out

Node limits concurrent connections

Lower canales in servers.json (try 2)

For a full protocol-level trace, the official inspector:

npx @modelcontextprotocol/inspector python mcp_server.py

Once connected, the first thing to ask your agent is: "list my servers".


Tools

Category

Tools

Inventory

listar_servidores

Read

listar, info, leer_archivo, buscar, listar_backups, descargar, descargar_carpeta, estado_servidor, leer_config_mundo

Write

escribir_archivo, reemplazar_en_archivo, crear_carpeta, subir_archivo, subir_carpeta, instalar_addon, configurar_mundo

Destructive

mover, borrar, restaurar_backup, controlar_servidor

Tool names and parameters are in Spanish because that is the project's working language and they form the stable public API. Descriptions and schemas guide the model, so you can prompt in any language — the agent maps your request onto the right call.

Notable behaviors worth knowing:

  • leer_archivo caps at 200 KB, reports truncado: true when it hits the limit, and detects binaries instead of dumping bytes into the model's context.

  • buscar walks at most 6 levels deep, returns at most 40 results, skips .git, node_modules and .ai_backups, and when filtering by content only scans files under 2 MB — reporting the first matching line number.

  • borrar requires recursivo: true for directories and flat-out refuses to delete the sandbox root.

  • instalar_addon uploads non-.zip files as-is, and for archives extracts locally then uploads the resulting tree.

  • configurar_mundo only writes keys that already exist in level.dat, keeping their original NBT type, and refuses to run while the panel reports the server as running.


How it works

Four layers, each with one job:

mcp_server.py    stdio MCP server: schemas, timeouts, destructive hints   <- entry point
   |
tools.py         tool catalog + dispatcher (shared with standalone mode)
   |
servers.py       multi-server registry, ${ENV} expansion, connection pool
   |          \         \
sftp_client.py  panel.py  level_dat.py
                            SFTPManager: sandbox, backups, parallel engine
                            PanelClient: power control over the panel HTTP API
                            level_dat:   Bedrock level.dat, little-endian NBT

install_mcp.py sits deliberately outside that stack. It imports nothing from the project and nothing outside the standard library, because its whole job is to run in an environment where the dependencies aren't installed yet and report why. It talks to mcp_server.py the same way a real client does — as a subprocess over stdio — rather than importing it.

mcp_server.py is async but paramiko is blocking, so every call is dispatched to a worker thread via asyncio.to_thread and wrapped in asyncio.wait_for with a per-tool budget (45 s for listar, 300 s for borrar, 900 s for instalar_addon, and so on). On timeout the connection is deliberately closed — a paramiko thread can't be killed, but killing its socket makes it abort instead of quietly consuming the channel forever.

One connection per channel, not one channel per connection

This is the non-obvious part, and it's the reason bulk operations are fast here.

SFTP has no recursive delete and no bulk upload: every single file costs a network round trip. Deleting a 2,000-file addon serially over a 120 ms link is four minutes of waiting. The obvious fix is to open several SFTP channels over the existing SSH transport.

That does not work. Pterodactyl-style panels run their own SFTP implementation which accepts exactly one channel per connection — requesting a second one doesn't error, it blocks forever. What they do accept is multiple simultaneous SSH connections.

So _canales() opens N full SSH connections (canales, default 8), and opens them concurrently, because each handshake costs ~2 s and doing ten in series would cost 20 s before any work begins. Each worker thread then owns one connection exclusively, so tasks are dealt out in fixed batches rather than pulled from a shared pool.

Extra connections are only worth their handshake above a threshold (umbral_paralelo, 25 items). Below that, everything runs on the primary connection.

Other things the engine does

  • Recursive delete walks the tree level by level, listing each level in parallel, then deletes all files in parallel, then removes directories deepest-first (siblings at the same depth are independent, so those go in parallel too). A cheap one-call probe short-circuits small flat folders back to the serial path.

  • Directory uploads build the complete file list before touching the network, then create parent directories shallowest-first, then push files in parallel. Empty directories are skipped entirely — some obfuscated addons ship thousands of decoy directories with no files, and creating them one by one costs minutes of network for nothing.

  • A _dirs_ok set caches directories already known to exist, so uploading a deep tree doesn't re-stat() the same parents dozens of times.

  • Keepalive packets every 15 s stop the transport from going zombie while idle — the symptom is nasty, since the socket still looks alive but every operation hangs until the timeout fires. On top of that, the sftp property checks transport liveness before each use and reconnects transparently.

  • Pterodactyl quirks are handled explicitly: it answers a generic failure instead of ENOENT for missing paths, so those errors are translated into readable messages.


A real walkthrough: installing a Bedrock addon

This is the workflow the project was actually built for, condensed. The full operational guide lives in docs/bedrock-addons-shaders.md.

Bedrock doesn't activate a pack just because the files are on disk — it has to be registered against the world. So it's a two-step job, and the second step is a JSON edit:

  1. Upload the packinstalar_addon with remoto: "behavior_packs" (or "resource_packs"). A .mcaddon carries both a behavior and a resource pack; treat them as two separate installs.

  2. Read the manifestleer_archivo on the uploaded manifest.json and take uuid and version from the header section, never from modules. Mixing those up is the source of the classic "Pack with id not found".

  3. Register it against the worldworlds/<world name>/world_behavior_packs.json. Use reemplazar_en_archivo to find the closing ] and splice the new entry in, rather than rewriting the file: that preserves every pack already installed.

  4. Enable the experiments it needs — a pack with a script module does nothing until Beta APIs is on. Stop the server, then configurar_mundo with experimentos: {"beta_apis": true}. See World settings.

  5. Restartcontrolar_servidor if a panel block is configured, otherwise from the panel by hand. Plain SFTP can't do it.

Troubleshooting with the tools

Symptom

How to diagnose

Fix

Pack with id not found

leer_archivo the uploaded manifest.json, compare the header uuid against what's registered

reemplazar_en_archivo to correct the pack_id

Nested path (behavior_packs/Pack/Pack/...)

listar on behavior_packs to see the real uploaded structure

mover to flatten, or borrar + reinstall

Players don't download textures

leer_archivo on server.properties, check texturepack-required

Set it to true; if the pack was updated, bump version in both the manifest and the world JSON

Works in singleplayer, not on the server

leer_config_mundo to see which Experimental Features are off — a script module needs gametest (Beta APIs)

Stop the server and turn them on with configurar_mundo

Server won't boot after an install

Enable content-log-file-enabled in server.properties, restart, then leer_archivo the new log

Remove the entry from the world JSON and borrar the pack folder

Need to undo anything

listar_backups, then restaurar_backup

What it explicitly won't do

  • No remote command execution, no remote unzip. instalar_addon extracts on your machine and uploads the tree.

  • No shader management. Shaders are 100% client-side (RenderDragon). There is no server-side file to upload, so if someone asks for "install a shader on the server", the correct answer is an explanation, not a folder.

  • No websocket setup. /wsserver is a per-player client setting; there is nothing in the world or the server to flip for it. Experiment toggles are editable — see World settings.


Restarting the server

SFTP genuinely cannot restart anything — but Pterodactyl-based panels expose a separate HTTP API that can, and sftp-manager speaks it. Add a panel block to a server and two more tools appear: estado_servidor (read-only) and controlar_servidor (destructive).

"panel": {
  "url": "https://panel.tarohosting.net",
  "api_key": "${TARO_PANEL_API_KEY}",
  "server_id": "1a2b3c4d"
}

The API key is a client key — Account → API Credentials in the panel, not an application key. It can only touch servers belonging to the account that created it. The server_id is the short id in the panel URL, not the display name — and on Pterodactyl it is also the suffix of your SFTP username, so a user of abc12345.f00d means the id is f00d. You already have it in the config file. In single-server mode the same thing is configured with PANEL_URL, PANEL_API_KEY and PANEL_SERVER_ID.

controlar_servidor takes encender, apagar, reiniciar or forzar_apagado (the last one kills the process without saving and can corrupt a world — it exists for when a normal stop hangs). The signal is asynchronous: the panel accepts it and the state changes a few seconds later, so the tool tells the model to confirm with estado_servidor rather than assuming.

Three details that matter in practice:

  • The SFTP connection is never opened for these two tools. A stopped server usually refuses SFTP, and controlar_servidor is exactly what you need in that situation — opening a connection first would make it unusable precisely when it matters.

  • Panel config is optional and stays optional. If you never add the block, nothing changes; the two tools return an error explaining how to enable them, and every file tool works as before. listar_servidores reports control_de_energia per server so the model knows whether it can offer a restart at all.

  • Configuration is read once, at startup. load_dotenv() and the server registry both run when the MCP process starts, and there is no hot reload. Adding a panel block to a server that is already running has no effect until you restart the MCP client. If the tools still report power control as unconfigured right after you edited servers.json, that is why: the file on disk is correct, the live process is holding the copy it read at boot.

World settings (level.dat)

server.properties is a text file any editor can touch. The settings that matter most for add-ons are not in it: gamerules and the experiment toggles live inside the world's level.dat, and the one everybody needs is Beta APIs — without it, no behavior pack with a script module ever runs. Two tools cover it: leer_config_mundo (read-only) and configurar_mundo (write, with backup).

leer_config_mundo                                  -> ajustes + experimentos + ruta
configurar_mundo experimentos={"beta_apis": true}  -> flips gametest, leaves a backup

The path is deduced from level-name in server.properties (falling back to the single folder under worlds/), so you rarely pass ruta by hand.

Why this needed a parser and not a text edit

Bedrock's level.dat is NBT, but not the NBT most libraries assume:

  1. An 8-byte header before the data: storage version (int32) + payload length (int32), both little-endian.

  2. No gzip. Java Edition compresses its NBT; Bedrock does not.

  3. Little-endian numbers throughout, not big-endian.

A plain nbtlib.load() assumes gzip + big-endian and doesn't fail — it silently parses an empty compound. Writing that back would wipe the world's ~115 keys (spawn, seed, gamerules). So level_dat.py splits the header by hand, parses the payload with an explicit byteorder="little", recomputes the length on the way out, and re-parses the bytes it produced before uploading them. Parsing an empty compound is treated as an error, not as an empty world.

Guardrails

  • Existing keys only. configurar_mundo refuses to invent keys, and coerces each new value into the type the key already had (Byte stays Byte, out-of-range values are rejected with the valid range). A key that differs only in case gets a "did you mean" hint.

  • Experiments are a nested compound, not root keys — experiments.gametest is what the game menu calls "Beta APIs" (the name is historical: the Scripting API was born inside the GameTest Framework). Turning any experiment on also sets experiments_ever_used and saved_with_toggled_experiments, without which the engine ignores the toggle on load.

  • The server must be stopped. Bedrock keeps the world in memory and rewrites level.dat when it saves, so a hot edit disappears with no error at all. If a panel block is configured, the tool checks the state and refuses; forzar: true overrides it.

  • Unknown toggles are written with a warning, not blocked — the list changes with every Bedrock release.

What it can't do

Websockets are not a world setting. /wsserver connections are configured per player in their own client settings; there is nothing in level.dat to flip for them.

Host key verification

Earlier versions accepted any host key without checking (AutoAddPolicy), which left the first connection open to interception. There are now three policies, set with host_key_policy:

Policy

Behavior

accept-new (default)

Trust on first use: record the key the first time, then require it to match. This is ssh's TOFU model — it protects against a key changing under you without breaking a fresh install.

strict

The key must already be in known_hosts. Nothing is trusted implicitly.

auto

Accept anything, remember nothing. The old behavior, kept only for disposable environments.

The default is deliberately not strict: that would break every existing setup on upgrade. If you want the strong guarantee, set strict and seed the file first:

ssh-keyscan -p 2022 node1.tarohosting.net >> ~/.ssh/known_hosts

When a key changes, the error says so explicitly rather than failing with a generic handshake message — a reinstalled node and an active MITM look identical to the client, and you should be the one deciding which it was.


Safety model

Handing an LLM write access to a live game server deserves more than good intentions.

Layer

What it stops

Path sandbox

Every path is resolved and normalized against the root before any network call. ../../etc/passwd is rejected locally, never sent.

Root delete guard

borrar refuses outright when the resolved path is the sandbox root.

Directory delete guard

Deleting a directory requires an explicit recursivo: true.

Automatic backups

Overwrites and deletes copy the original into .ai_backups/ on the server, timestamped. listar_backups enumerates, restaurar_backup rolls back.

Human confirmation

Write and delete tools carry destructiveHint, so the client prompts with the exact arguments. Standalone mode prompts y/N in the terminal.

Ambiguity refusal

reemplazar_en_archivo fails when the fragment matches more than once and todas is false, rather than picking one.

Zip-Slip protection

Archive members with absolute paths or .. segments are rejected before extraction.

NBT sanity checks

level.dat writes refuse an empty or inconsistent parse, keep each key's original NBT type, reject keys that don't already exist, and re-parse the produced bytes before uploading them.

Live-world guard

configurar_mundo refuses to edit level.dat while the panel reports the server running — Bedrock would overwrite the change on save without any error.

Read limits

200 KB per file plus binary detection, so a 40 MB latest.log can't blow up the agent's context.

Per-tool timeouts

No call hangs forever; the connection is recycled and an explicit error returned.

Host key verification

Trust-on-first-use by default, strict available. A changed key aborts the connection with an explanation.

Every row above is covered by a test — see Tests.


Standalone mode (optional)

Prefer a terminal over an MCP client? cli.py runs its own tool-use loop against the Anthropic API, with the same tools and a y/N prompt on anything destructive. It needs ANTHROPIC_API_KEY in your .env and uses the single-server environment config.

python cli.py

Commands: /salir, /reset, /backups, /ayuda.

Mode

Who drives the agent

When to use it

MCP (mcp_server.py)

Your MCP client

The normal path. No API key of your own.

Standalone (cli.py)

The loop in agent.py

No MCP client available. Requires ANTHROPIC_API_KEY.


Tests

pip install -r requirements-dev.txt
python -m pytest

206 tests. No network and no real server involved: tests/conftest.py provides an in-memory double of paramiko's SFTP client, and the panel API is exercised by intercepting urlopen. The suite runs on Linux and Windows across Python 3.10 and 3.13 in CI — Windows is there on purpose, since the client juggles local and remote path separators and that is exactly where it breaks.

The one exception to "no real server" is test_install_mcp.py, which launches mcp_server.py as a subprocess and speaks MCP to it over stdio. It still touches no network: SFTP connections are lazy, so the handshake and the tool listing complete without any credentials.

What's covered:

File

Focus

test_sandbox.py

Path traversal in every shape, including lookalike prefixes like /home/container2

test_client.py

Backups, surgical edits, ambiguity refusal, recursive delete, Zip-Slip, empty-directory skipping, the directory cache

test_config.py

Parity between servers.json and .env for every tunable, validation, unknown-key rejection

test_panel.py

Every power signal, HTTP error translation, response parsing

test_tools.py

Wiring: no tool can ship without a dispatcher, a timeout and an MCP function

test_level_dat.py

level.dat: header round-trip, empty-NBT refusal, NBT type coercion, experiment bookkeeping, the server-is-running guard

test_install_mcp.py

Install: absolute interpreter paths, merging a client config without losing entries, no secret ever reaching the diagnostic output, and a live MCP handshake

That last file is worth calling out: the "easy to forget" steps in Adding a tool are now enforced by tests rather than by a warning in a README nobody re-reads.

Contributing

The repo is private, so issues and pull requests are open to invited collaborators. Bug reports, new tools, support for other panel APIs and translations are all welcome. CI runs the suite on every pull request.

If you're an AI agent working on this codebase, read AGENTS.md first — it carries the conventions and the safety rules that aren't obvious from the code.

Adding a tool

  1. Add the method to SFTPManager in sftp_client.py.

  2. Add an entry to TOOLS in tools.py with its input_schema.

  3. Wire it into build_dispatch.

  4. If it modifies anything, add it to the DESTRUCTIVAS set.

  5. Add an @app.tool(...) function in mcp_server.py delegating to _run("name", servidor, ...).

  6. Give it a timeout budget in TIMEOUTS.

Steps 4 and 6 are easy to forget and both matter: skipping 4 means the standalone CLI won't ask for confirmation, and skipping 6 silently falls back to a 60 s budget that a bulk operation will blow through. tests/test_tools.py fails if you miss any of them, so CI catches it before a reviewer has to.

Ideas worth picking up

  • Per-tool progress reporting for long uploads (MCP supports progress notifications).

  • Support for panels that aren't Pterodactyl-compatible.

  • Reading world_behavior_packs.json as a first-class operation instead of raw text edits.

Using OpenRouter instead of the Anthropic API

The schemas are OpenAI-compatible by renaming input_schemaparameters. Swap the client in agent.py for openai.OpenAI(base_url="https://openrouter.ai/api/v1") and adapt the loop to tool_calls / role: "tool".

Security

Never commit .env or servers.json — both are git-ignored by default. If you think you've leaked a credential, rotate the panel password immediately; deleting the file in a later commit does not remove it from git history.

A private repo is not a secret store. Visibility can be flipped back, forks keep their copies, and every collaborator has full history. Treat a committed credential as compromised regardless of who could see the repo at the time.

When an agent installs this for you, it should ask you for hosts and usernames and write those into servers.json as ${VAR} references — then let you put the actual passwords in .env. Passwords should not travel through a chat transcript.

Found a security issue? Open an issue describing the impact, without including credentials.

License

MIT © Gustavo Reyes

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    An MCP server that gives AI assistants full SSH/SFTP remote operations — session management, command execution, interactive shells, file transfers, port forwarding, and system diagnostics.
    2
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    MCP server giving AI agents full SSH access with persistent sessions, structured command output, SFTP file transfer, and port forwarding.
    18
    10
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    An open MCP server that gives any AI agent SSH access to remote Linux/Unix machines — shell commands, file read/write, and SFTP transfers.
    11
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A security-first MCP server that gives AI assistants controlled, safe access to manage remote servers via SSH with whitelisted operations and no generic command execution.
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Gus2708/sftp-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server