Skip to main content

Tools Reference

IdeGYM servers expose HTTP endpoints and MCP tools. This page covers every tool category available on a running server, how to call each one, and what plugins are required.

To discover the tools available on a specific running server at runtime, use the orchestrator MCP list_server_mcp_tools tool. See MCP Server — Server-side MCP.


Default Tools​

Every IdeGYM server includes the default tools plugin. These tools are available on all images regardless of which IDE or additional plugins are installed.

The file tools (create_file, edit_file, patch_file) are also exposed as MCP tools on the server's /mcp endpoint, so they can be called via call_server_mcp_tool without going through the HTTP API.

Bash​

Execute a bash script inside the container.

Endpoint: POST /api/tools/bash

Request:

FieldTypeDefaultDescription
commandstring—Bash script to execute
cwdstring or nullnullWorking directory; a relative path resolves against the project directory
envobject{}Environment variables added to the command's environment
userstring or nullnullRun the command as this user; needs a root server or passwordless sudo
timeoutfloat600.0Maximum execution time in seconds
graceful_termination_timeoutfloat2.0Seconds to wait for graceful process exit before SIGKILL
max_output_bytesinteger or null1048576Maximum retained bytes per stream; null retains complete output
strip_outputboolfalseTrim leading and trailing whitespace from stdout and stderr

Response:

FieldTypeDescription
stdoutstringRetained standard output
stderrstringRetained standard error
exit_codeintExit code returned by the process

The default limit applies independently to stdout and stderr. Truncated output keeps its beginning and end and includes a marker with the number of omitted bytes; the marker itself is outside the configured limit. Set max_output_bytes to null only when complete output is required and its size is trusted. IdeGYM still drains all produced output until the command finishes or reaches its execution timeout so subprocess pipes cannot deadlock.

How the script is run​

The script is written to a temp file inside the container, and bash -c evaluates the file's contents. There is no practical size limit: passing the script itself as the bash -c argument used to cap it at the kernel's MAX_ARG_STRLEN of 128 KiB, and an oversized script failed with a bare E2BIG.

It still behaves like bash -c: $0 is bash, there are no positional parameters, and an error reads bash: line 3: nosuchcmd: command not found. Running the file as bash <file> would have put the temp file's name, different on every call, in both places.

A file rather than bash's stdin is deliberate. A script read from stdin is consumed incrementally, so any command inside it that reads stdin — cat, read, an interactive installer — would swallow the rest of the script. Running from a file leaves the command's own stdin alone.

The temp file is removed once the command finishes, including when it times out or the request is cancelled.

Before the script, IdeGYM sources a bundled init file that sets up the shell environment. It is joined to your script with ;, not &&, so it cannot change what your script means: every statement runs exactly as written, and your own && and || behave normally. The prefix stays on the same line, so a bash error still reports the line number you wrote. An empty script is a no-op that exits 0.

The init sources ~/.bashrc, and its exit status is ignored: a .bashrc whose last line is a routine [ -f ~/.fzf.bash ] && source ~/.fzf.bash must not fail every command. Only an init file that cannot be read at all aborts the command, with exit code 1 and a message on stderr.

Per-command context​

Without cwd, env and user the only way to set context is to write it into the script — cd -- … || exit 1, one export per variable, a runuser wrapper — which every client would have to generate correctly, and which spends the script's own size budget.

result = await server.execute_bash(
"python -m pytest -q",
cwd="tests", # relative to the project directory
env={"PYTHONHASHSEED": "0"}, # merged over the cleaned environment
user="devuser", # needs a root server, or passwordless sudo as in the server image
)

The environment the command starts from is the server's own, with IdeGYM-internal entries stripped; env is merged over it, so a name that already exists is overridden. Values passed this way never enter the command text, which means they are not written to the command log — prefer it to an export line for anything sensitive.

A PATH in env governs the commands your script runs, not how IdeGYM starts it: bash itself is resolved once against the server's own PATH, so env={"PATH": "/opt/tool/bin"} is safe.

Names must be shell identifiers ([A-Za-z_][A-Za-z0-9_]*) and values must not contain NUL; any other entry is rejected with 422 Unprocessable Entity before the command runs.

user runs the script as that user, with the environment described above plus the user's own HOME, USER, LOGNAME and SHELL. Naming the server's own user is not a switch at all. A root server drops privileges with runuser --preserve-environment; the server image runs as the non-root appuser with passwordless sudo, so there the executor goes through sudo to reach the same runuser, restoring the environment sudo would otherwise rewrite (PATH, LD_*, PYTHONPATH) and signalling the command's process group through sudo on timeout. A server that has neither rejects the request with 400 Bad Request rather than running anything. An unknown user is a 400 too.

The temp file holding the script stays private to the server's user (mode 0600) even then. Bash receives it as a descriptor opened before the privilege drop, so the target user can run the script without being able to open the file, and neither can anyone else in the container.

Output fidelity​

Within the retained window, output is returned exactly as the command wrote it. Nothing is trimmed, so a trailing newline that belongs to a git diff survives and printf 'x' stays distinguishable from printf ' x '. Set strip_output to true if you would otherwise call .strip() on the result yourself.

Bytes that are not valid UTF-8 are replaced with U+FFFD rather than failing the request, so cat on a binary file or a compiler emitting latin-1 diagnostics still returns the command's real exit code. The response is JSON text: to move binary data out of the container intact, encode it inside the command (base64 -w0 …) or use the file endpoints.

What gets logged​

The server never writes a command or its output to the log at INFO. An INFO record carries the shape of the call only — the command's length on the way in, then the exit code and the byte count of each stream on the way out.

The command itself and excerpts of both streams are logged at DEBUG, and the command passes through a redaction step first that replaces the value of every export NAME=... assignment with <redacted>. Raise the level with IDEGYM_LOG_LEVEL=DEBUG when you need to see what a sandbox actually ran, and be aware that anything a script sets by other means — an inline NAME=value cmd prefix, a heredoc, a literal in an argument — is not redacted.

Via orchestrator MCP (run_bash_command):

command = await mcp.call_tool(
"run_bash_command",
{
"request": {
"client_id": client_id,
"server_id": server_id,
"command": "python -c 'import sys; print(sys.version)'",
"command_timeout": 30.0,
"max_output_bytes": 1048576,
"env": {"API_TOKEN": token}, # kept out of the command text, as over HTTP
},
},
)

The MCP tool takes the same cwd, env, user and strip_output fields as the HTTP request, with the same meaning; only the timeout is named command_timeout.

Via Python client:

response = await server.execute_bash("python -c 'import sys; print(sys.version)'")
print(response.stdout)

Create File​

Create a new file with given content.

Endpoint: POST /api/tools/file/create · MCP tool: create_file

Request:

FieldTypeDescription
file_pathstringAbsolute path where the file will be created
contentstringFile content

Via server MCP:

result = await mcp.call_tool(
"call_server_mcp_tool",
{
"request": {
"client_id": client_id,
"server_id": server_id,
"tool_name": "create_file",
"arguments": {"request": {"file_path": "/root/work/hello.py", "content": "print('hello')\n"}},
},
},
)

Edit File​

Replace a range of lines in an existing file.

Endpoint: POST /api/tools/file/edit · MCP tool: edit_file

Request:

FieldTypeDescription
file_pathstringAbsolute path to the file
start_lineintFirst line to replace (1-indexed)
end_lineintLast line to replace (inclusive)
new_contentstringReplacement content for the specified range

Patch File​

Apply a unified diff patch to a file.

Endpoint: POST /api/tools/file/patch · MCP tool: patch_file

Request:

FieldTypeDescription
file_pathstringAbsolute path to the file
patchstringUnified diff patch to apply

Upload File Chunk​

Write one base64-encoded chunk of raw bytes into a file. This is the binary-safe way in: the orchestrator stores and replays a forwarded request as JSON text, so base64 is what survives the round trip.

Endpoint: POST /api/tools/file/upload

Request:

FieldTypeDefaultDescription
file_pathstring—Absolute path to write to; missing parent directories are created
content_base64string—Base64-encoded bytes to write at offset
offsetint0Byte offset in the file at which to write this chunk
truncatebooltrueCut the file off at the end of this chunk once written

Response: file_path, bytes_written, size (file size after the write).

truncate is what keeps a re-upload honest: with it on, writing a shorter file cannot leave the tail of a longer previous one behind. Turn it off for an out-of-order write. Writing past the current end of the file leaves a hole of zero bytes, as lseek does.

Download File Chunk​

Read one base64-encoded chunk of raw bytes from a file.

Endpoint: POST /api/tools/file/download

Request:

FieldTypeDefaultDescription
file_pathstring—Absolute path to read from
offsetint0Byte offset to read from
lengthint1048576Maximum number of raw bytes to read

Response: file_path, offset, content_base64, bytes_read, size (total file size), and eof (true when this chunk reaches the end). A missing path returns 404; a directory returns 400.

Via Python client: the client wraps both endpoints in upload_file / upload_bytes / download_file / download_bytes, which do the chunking for you. See Binary file transfer.


IDE Inspection (inspect.sh)​

The Idea and PyCharm plugins expose an inspection endpoint backed by the JetBrains command-line inspector (inspect.sh). It runs the full IntelliJ static analysis pipeline and writes results to a directory inside the container.

Requires: Idea or PyCharm plugin in the image build pipeline.

IDEA Endpoint​

Endpoint: POST /idea/inspect

PyCharm Endpoint​

Endpoint: POST /pycharm/inspect

Request​

FieldTypeDefaultDescription
project_pathstring—Absolute path to the project directory inside the container
profile_pathstring—Absolute path to an inspection profile XML (e.g. .idea/inspectionProfiles/Project_Default.xml)
output_dirstring—Directory where result files will be written (created if absent)
changes_onlyboolfalseOnly inspect locally changed files (-changes)
directorystringnullLimit scope to this subdirectory (-d)
formatstring"xml"Output format: "xml" or "json"
verbosityint0Verbosity level 0–2 (-v0 / -v1 / -v2)
timeoutfloat600.0Maximum seconds to wait for inspect.sh

Response​

FieldTypeDescription
output_dirstringDirectory containing the result files
exit_codeintExit code from inspect.sh (0 = success)
stdoutstringStandard output
stderrstringStandard error

Result files (XML or JSON) are written to output_dir inside the container. Read them afterwards with a bash tool call:

results = await server.execute_bash(f"cat {output_dir}/*.xml")

Via Python Client​

from idegym.client.client import IdeGYMClient

client = IdeGYMClient(...)
server = await client.start_server(...)

# IDEA
response = await server.idea.inspect(
project_path="/root/work",
profile_path="/root/work/.idea/inspectionProfiles/Project_Default.xml",
output_dir="/tmp/inspection-results",
format="xml",
timeout=300.0,
)
print("exit_code:", response.exit_code)

# Read results
results = await server.execute_bash("cat /tmp/inspection-results/*.xml")
print(results.stdout)

Replace server.idea with server.pycharm when using the PyCharm plugin.

Via Orchestrator MCP​

import json

forward = await mcp.call_tool(
"forward_request",
{
"request": {
"client_id": client_id,
"server_id": server_id,
"path": "idea/inspect",
"method": "POST",
"headers": {"Content-Type": "application/json"},
"body": json.dumps(
{
"project_path": "/root/work",
"profile_path": "/root/work/.idea/inspectionProfiles/Project_Default.xml",
"output_dir": "/tmp/inspection-results",
"format": "xml",
}
),
},
},
)

mcp-steroid Tools​

mcp-steroid is a JetBrains plugin that runs an MCP server inside the IDE JVM. It provides direct access to the IntelliJ Platform API: project model, semantic index, PSI tree, test runner, debugger, and VCS layer.

Requires: Idea or PyCharm plugin with mcp_steroid=True.

from idegym.plugins.pycharm.image import PyCharm

PyCharm(version="2026.1.1", mcp_steroid=True)

Ports:

  • Inside container: 127.0.0.1:6315 (MCP endpoint: /mcp)
  • External bridge: 0.0.0.0:6316
  • MCP transport: streamable HTTP (not SSE)

Namespace in server MCP proxy: tools are accessible through the server's /mcp endpoint prefixed with the plugin type name (e.g. pycharm_steroid_open_project when the PyCharm plugin declares the upstream).

MCP Tools​

mcp-steroid exposes 9 MCP tools:

ToolDescription
steroid_open_projectOpen a project in the IDE by path
steroid_list_projectsList projects currently known to the IDE
steroid_list_windowsList open editor windows
steroid_execute_codeExecute Kotlin code in the IDE JVM with full IntelliJ Platform API access
steroid_take_screenshotTake a screenshot of the IDE window
steroid_inputSend keyboard or mouse input to the IDE
steroid_action_discoveryDiscover available IDE actions
steroid_fetch_resourceFetch an MCP resource by URI (LSP, refactoring, debugging, testing, VCS)
steroid_execute_feedbackProvide feedback on executed operations

MCP Resources​

mcp-steroid also exposes 58+ MCP resources covering:

  • LSP — diagnostics, hover, go-to-definition, find-references
  • Refactoring — rename, move, extract
  • Debugging — breakpoints, step execution, variable inspection
  • Testing — run/inspect test results
  • VCS — git status, diff, history

Accessing mcp-steroid via Orchestrator MCP​

Use list_server_mcp_tools and call_server_mcp_tool to reach mcp-steroid tools through the orchestrator without a direct network connection to the container.

List all available tools (includes mcp-steroid tools prefixed with the plugin namespace):

tools_result = await mcp.call_tool(
"list_server_mcp_tools",
{"request": {"client_id": client_id, "server_id": server_id}},
)
for tool in tools_result.structured_content["tools"]:
print(tool["name"])
# pycharm_steroid_open_project
# pycharm_steroid_list_projects
# ...

Open a project:

result = await mcp.call_tool(
"call_server_mcp_tool",
{
"request": {
"client_id": client_id,
"server_id": server_id,
"tool_name": "pycharm_steroid_open_project",
"arguments": {"path": "/root/work"},
},
},
)

Execute Kotlin in the IDE JVM:

result = await mcp.call_tool(
"call_server_mcp_tool",
{
"request": {
"client_id": client_id,
"server_id": server_id,
"tool_name": "pycharm_steroid_execute_code",
"arguments": {
"code": "project.name",
},
},
},
)

Accessing mcp-steroid Directly (Docker)​

For standalone Docker deployments, the socat bridge exposes port 6316:

docker run -p 6316:6316 <image>
# MCP endpoint: http://localhost:6316/mcp
from fastmcp import Client

async with Client("http://localhost:6316/mcp") as mcp:
tools = await mcp.list_tools()
result = await mcp.call_tool("steroid_list_projects", {})

Discovering Tools at Runtime​

Use the orchestrator MCP to discover which tools are available on a running server without knowing in advance which plugins are installed:

tools_result = await mcp.call_tool(
"list_server_mcp_tools",
{"request": {"client_id": client_id, "server_id": server_id}},
)
available = {t["name"] for t in tools_result.structured_content["tools"]}

if "pycharm_steroid_open_project" in available:
print("mcp-steroid is available on this server")

See MCP Server for the full orchestrator MCP reference.