Skip to main content

MCP Server

IdeGYM exposes an MCP server for tool-based access to orchestrator operations. This is useful for agents and other clients that discover and call tools instead of using the REST API directly.

The MCP server is available from the orchestrator at:

/mcp

Use the same host as the HTTP API. For example:

  • Local e2e deployment: http://idegym-local.test/mcp
  • Remote deployment: https://idegym.yourdomain.com/mcp

The MCP server does not keep separate state. It is a thin layer over the orchestrator: tools call the same client, server, forwarding, async operation, and Kaniko build handlers as the REST API.


Authentication​

The MCP app is mounted inside the orchestrator FastAPI service. In the manifests in this repository, the orchestrator does not install an application-level Basic Auth middleware and does not set IDEGYM_AUTH_USERNAME or IDEGYM_AUTH_PASSWORD.

If your deployment protects the orchestrator behind an ingress, reverse proxy, or other gateway, connect to /mcp with the same credentials required for the HTTP API. If the orchestrator is exposed without external authentication, omit the auth argument.

import asyncio

import httpx
from fastmcp import Client


async def main():
auth = httpx.BasicAuth(username="admin", password="your-password")
async with Client("https://idegym.yourdomain.com/mcp", auth=auth, timeout=600.0) as mcp:
tools = await mcp.list_tools()
print([tool.name for tool in tools])


asyncio.run(main())

Tool Model​

FastMCP exposes IdeGYM request models under a top-level request argument. For example, register_client uses the same fields as the REST POST /api/clients request:

result = await mcp.call_tool(
"register_client",
{
"request": {
"name": "agent-run-1",
"namespace": "idegym",
"nodes_count": 1,
},
},
)
client_id = result.structured_content["id"]

Long-running tools return an operation ID. Poll get_operation_status until the operation reaches SUCCEEDED or a terminal failure state. When the operation succeeds, result contains the JSON-serialized final response.

import asyncio


async def wait_for_operation(mcp, operation_id: int, poll_interval: float = 1.0):
while True:
status = await mcp.call_tool(
"get_operation_status",
{"request": {"operation_id": operation_id}},
)
operation_status = status.structured_content["status"]

if operation_status == "SUCCEEDED":
return status.structured_content
if operation_status in {"FAILED", "CANCELLED"}:
raise RuntimeError(f"Operation {operation_id} ended with {operation_status}: {status.structured_content}")

await asyncio.sleep(poll_interval)

Available Tools​

ToolDescription
register_clientCreate a client record, optionally with node pre-provisioning
stop_clientStop a client and delete its alive server resources
finish_clientMark a client and its alive servers reusable without deleting resources
start_serverStart a server pod or reuse a matching finished server
stop_serverStop a server and delete its Kubernetes resources
finish_serverMark a server reusable without deleting Kubernetes resources
restart_serverRestart server pods and wait for readiness
build_images_from_yamlStart Kaniko image build jobs from image-builder YAML
get_operation_statusRead the status and result of an async operation
get_job_statusRead the status and image tag for a Kaniko build job
forward_requestForward an HTTP request to a running IdeGYM server
run_bash_commandExecute a bash script on a running IdeGYM server
list_server_mcp_toolsList all MCP tools exposed by a running IdeGYM server
call_server_mcp_toolCall an MCP tool on a running IdeGYM server by name

forward_request.path is a server-internal path without a leading slash, for example api/tools/bash. The orchestrator forwards it to http://{server-service}/{path} inside the Kubernetes cluster.


Server Lifecycle Example​

Start a server with reuse enabled:

import json

start = await mcp.call_tool(
"start_server",
{
"request": {
"client_id": client_id,
"namespace": "idegym",
"image_tag": "registry.example.com/my-env:latest",
"server_name": "my-server",
"runtime_class_name": "gvisor",
"reuse_strategy": "RESTART",
},
},
)
start_status = await wait_for_operation(mcp, start.structured_content["operation_id"])

start_response = json.loads(start_status["result"])
server_id = start_response["server_id"]

start_status["result"] contains the JSON-serialized StartServerResponse.

Run a command on the server:

command = await mcp.call_tool(
"run_bash_command",
{
"request": {
"client_id": client_id,
"server_id": server_id,
"command": "python -c 'print(\"hello\")'",
"command_timeout": 60.0,
"max_output_bytes": 1048576,
},
},
)
command_operation_id = command.structured_content["async_operation_id"]
command_status = await wait_for_operation(mcp, command_operation_id)

forward_response = json.loads(command_status["result"])
bash_response = json.loads(forward_response["body"])
print(bash_response["stdout"])
print(bash_response["stderr"])
print(bash_response["exit_code"])

max_output_bytes limits retained stdout and stderr independently. It defaults to 1 MiB; set it to null only when complete output is required and its size is trusted. Truncated streams preserve their beginning and end with an omitted-byte marker.

run_bash_command also accepts cwd, env, user and strip_output, with the same meaning and validation as on POST /api/tools/bash — see Tools Reference — Bash. Pass secrets through env rather than in command, so they stay out of the command text.

To keep a server available for reuse, call finish_server. A later start_server call with matching parameters and reuse_strategy: "RESTART" or "RESET" can reuse the same server.

await mcp.call_tool(
"finish_server",
{
"request": {
"client_id": client_id,
"namespace": "idegym",
"server_id": server_id,
},
},
)

To delete the Kubernetes resources instead, call stop_server.


Server-side MCP​

Every running IdeGYM server exposes its own MCP endpoint at /mcp. The orchestrator's list_server_mcp_tools and call_server_mcp_tool tools bridge to this endpoint, so agents can discover and invoke server-side tools without an additional network hop.

How it works​

Each IdeGYM server runs a FastMCP proxy that reads upstream declarations from /etc/idegym/mcp-upstreams.d/*.json. Each file names an upstream:

{"url": "http://localhost:6315/mcp"}

The file's stem (e.g. pycharm) becomes the namespace prefix in the proxy. All tools from that upstream are reachable through the server's /mcp endpoint with the namespace prefix applied (e.g. pycharm_steroid_open_project).

Image-build plugins declare their MCP upstream via get_mcp_upstream(). Image.to_spec() writes the config file automatically. See Plugins for details.

Built-in server tools​

Every IdeGYM server exposes these tools at /mcp regardless of which plugins are installed:

ToolDescription
create_fileCreate a new file with the given content
edit_fileReplace a line range in an existing file (1-indexed, inclusive)
patch_fileApply a unified diff patch to an existing file

Additional tools appear when image-build plugins declare MCP upstreams (e.g. IDE plugins, mcp-steroid). See Tools Reference for the full catalogue.

Listing tools on a running server​

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

Each entry has name, description (may be absent), and input_schema (JSON Schema object).

Calling a tool on a running server​

result = await mcp.call_tool(
"call_server_mcp_tool",
{
"request": {
"client_id": client_id,
"server_id": server_id,
"tool_name": "pycharm_steroid_list_projects",
"arguments": {},
},
},
)
content = result.structured_content["content"] # list of MCP content items
is_error = result.structured_content["is_error"]

content is a list of MCP content items (text, images, etc.) as returned by the tool. Check is_error to distinguish tool-level errors from transport errors.


JetBrains IDE MCP Endpoints​

The Idea and PyCharm image-build plugins install a JetBrains MCP server into the container. Two modes are available: the bundled JetBrains plugin and mcp-steroid.

Bundled JetBrains MCP plugin​

When open_project=True and a Project plugin precedes the IDE plugin in the build pipeline, the bundled JetBrains MCP plugin is activated. It binds to 127.0.0.1:64342 (loopback only).

At runtime, the startup script starts a socat bridge on 0.0.0.0:64343, making the server reachable from outside the container. For standalone Docker deployments:

docker run -p 64343:64343 <image>
# connect to: http://localhost:64343/sse or http://localhost:64343/stream

Both /sse (legacy) and /stream (newer versions) endpoints support the same MCP protocol. The startup scripts automatically detect which endpoint is available.

The plugin is declared as an MCP upstream on port 64342. When accessed through list_server_mcp_tools / call_server_mcp_tool, its tools are namespaced as idea_* or pycharm_* depending on the plugin type.

mcp-steroid​

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

Enable it by passing mcp_steroid=True to the Idea or PyCharm plugin:

from idegym.image.builder import Image
from idegym.plugins.pycharm.image import PyCharm

image = Image(base="...").plugin(PyCharm(version="2026.1.1", mcp_steroid=True))

When mcp_steroid=True:

  • mcp-steroid binds to 127.0.0.1:6315; a socat bridge re-listens on 0.0.0.0:6316

  • get_mcp_upstream() advertises port 6315 instead of 64342

  • If open_project=False (or no Project plugin in the pipeline), the IDE starts without a project — agents can open one at runtime via the steroid_open_project tool

  • The plugin version can be pinned with mcp_steroid_version (format: X.Y or X.Y.Z, optionally followed by lowercase alphanumeric -suffix segments — e.g. 0.94.0-8682a5ce, 0.100-409f23a2 or 0.102.0-r-c68d8f15d)

  • mcp_steroid_url takes an explicit .zip download link and overrides the URL built from the version. The release tag is inferred from the version's suffix shape — a multi-segment suffix means a two-part tag (0.102.0-r-c68d8f15d → v0.102), a single-segment one the full number (0.94.0-8682a5ce → v0.94.0) — so this is the escape hatch for a release that breaks that rule again, and for serving the plugin from an internal mirror. Put no credential in the URL: it is written into the Dockerfile verbatim. An authenticated download belongs in external_plugins, whose auth_env keeps the secret out of the image layer and the build log.

    PyCharm(
    mcp_steroid=True,
    mcp_steroid_version="0.102.0-r-c68d8f15d",
    mcp_steroid_url="https://github.com/jonnyzzz/mcp-steroid/releases/download/v0.102/mcp-steroid-0.102.0-r-c68d8f15d.zip",
    )

See Tools Reference for the full list of mcp-steroid tools and resources.