Sftp Manager
FreeNot checkedMCP server providing safe, sandboxed SFTP access to game servers for AI agents, enabling config editing, addon installation, and backups without shell access.
About
MCP server providing safe, sandboxed SFTP access to game servers for AI agents, enabling config editing, addon installation, and backups without shell access.
README
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 PRs welcome
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.
Features
- 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 aservidorparameter. - Host key verification — trust-on-first-use by default,
strictwhen 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 edits —
reemplazar_en_archivorefuses 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/.mcaddonarchives 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)
Install
git clone https://github.com/Gus2708/sftp-manager.git
cd sftp-manager
pip install -r requirements.txt
cp servers.example.json servers.json
Only
paramiko,python-dotenvandmcpare needed for the MCP server.anthropicis required exclusively for the optional standalone CLI.
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.
Installing the MCP server in your editor / agent
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.
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.
To register it globally instead:
claude mcp add sftp-manager --scope user -- python /absolute/path/to/sftp-manager/mcp_server.py
Verify with /mcp inside a session.
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. To debug a
connection without an editor in the way, run 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, 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_archivocaps at 200 KB, reportstruncado: truewhen it hits the limit, and detects binaries instead of dumping bytes into the model's context.buscarwalks at most 6 levels deep, returns at most 40 results, skips.git,node_modulesand.ai_backups, and when filtering by content only scans files under 2 MB — reporting the first matching line number.borrarrequiresrecursivo: truefor directories and flat-out refuses to delete the sandbox root.instalar_addonuploads non-.zipfiles as-is, and for archives extracts locally then uploads the resulting tree.configurar_mundoonly writes keys that already exist inlevel.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
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_okset 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
sftpproperty checks transport liveness before each use and reconnects transparently. - Pterodactyl quirks are handled explicitly: it answers a generic
failureinstead ofENOENTfor 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:
- Upload the pack —
instalar_addonwithremoto: "behavior_packs"(or"resource_packs"). A.mcaddoncarries both a behavior and a resource pack; treat them as two separate installs. - Read the manifest —
leer_archivoon the uploadedmanifest.jsonand takeuuidandversionfrom theheadersection, never frommodules. Mixing those up is the source of the classic "Pack with id not found". - Register it against the world —
worlds/<world name>/world_behavior_packs.json. Usereemplazar_en_archivoto find the closing]and splice the new entry in, rather than rewriting the file: that preserves every pack already installed. - Enable the experiments it needs — a pack with a
scriptmodule does nothing until Beta APIs is on. Stop the server, thenconfigurar_mundowithexperimentos: {"beta_apis": true}. See World settings. - Restart —
controlar_servidorif apanelblock 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_addonextracts 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.
/wsserveris 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. 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.
Two details that matter in practice:
- The SFTP connection is never opened for these two tools. A stopped server usually refuses
SFTP, and
controlar_servidoris 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_servidoresreportscontrol_de_energiaper server so the model knows whether it can offer a restart at all.
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:
- An 8-byte header before the data: storage version (int32) + payload length (int32), both little-endian.
- No gzip. Java Edition compresses its NBT; Bedrock does not.
- 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_mundorefuses to invent keys, and coerces each new value into the type the key already had (BytestaysByte, 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.gametestis 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 setsexperiments_ever_usedandsaved_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.datwhen it saves, so a hot edit disappears with no error at all. If apanelblock is configured, the tool checks the state and refuses;forzar: trueoverrides 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
126 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.
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 |
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
Issues and pull requests are welcome — bug reports, new tools, support for other panel APIs, translations. CI runs the suite on every pull request.
Adding a tool
- Add the method to
SFTPManagerinsftp_client.py. - Add an entry to
TOOLSintools.pywith itsinput_schema. - Wire it into
build_dispatch. - If it modifies anything, add it to the
DESTRUCTIVASset. - Add an
@app.tool(...)function inmcp_server.pydelegating to_run("name", servidor, ...). - 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.jsonas a first-class operation instead of raw text edits.
Using OpenRouter instead of the Anthropic API
The schemas are OpenAI-compatible by renaming input_schema → parameters. 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.
Found a security issue? Open an issue describing the impact, without including credentials.
License
MIT © Gustavo Reyes
Installing Sftp Manager
This server has no published package — it is built from source. Open the repository and follow its README.
▸ github.com/Gus2708/sftp-managerFAQ
Is Sftp Manager MCP free?
Yes, Sftp Manager MCP is free — one-click install via Unyly at no cost.
Does Sftp Manager need an API key?
No, Sftp Manager runs without API keys or environment variables.
Is Sftp Manager hosted or self-hosted?
A hosted option is available: Unyly runs the server in the cloud, no local setup required.
How do I install Sftp Manager in Claude Desktop, Claude Code or Cursor?
Open Sftp Manager on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
Fetch
Web content fetching and conversion for efficient LLM usage.
AWS KB Retrieval
Retrieval from AWS Knowledge Base using Bedrock Agent Runtime.
by modelcontextprotocolSpring AI MCP Server
Provides auto-configuration for setting up an MCP server in Spring Boot applications.
llm-analysis-assistant
A very streamlined mcp client that supports calling and monitoring stdio/sse/streamableHttp, and can also view request responses through the /logs page. It also
by xuzexin-hzCompare Sftp Manager with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All ai MCPs
