| name | generate-openenv-env |
| description | Builds an OpenEnv (Meta) variant of an RL environment. Use whenever someone asks to scaffold an OpenEnv server, port an existing env to OpenEnv, add MCP tools to an env, or deploy an OpenEnv to HF Spaces. OpenEnv is the right framework when the user wants HTTP+MCP, structured tool calls discovered via `list_tools()`, an optional Gradio UI, sandbox-backed sessions, or deployment as a Docker container / HF Space. Output is a runnable `<env_dir>/openenv/` folder with `server/app.py`, `server/<env>_environment.py`, `pyproject.toml`, `Dockerfile`, and `rollout.py`. Use for prompts like "wrap my game in OpenEnv", "make an MCP env for X", or "add the openenv variant". |
generate-openenv-env
Build the OpenEnv variant of an env. Targets OpenEnv >= 0.2.3 (openenv-core[core]).
Concept
OpenEnv is an HTTP server exposing tools via the MCP (Model Context Protocol) shape. The runtime is FastAPI; tools are FastMCP-decorated functions. Clients discover tools via list_tools() (under the hood: a list-tools action on /step) and call them via call_tool(name, **args).
When the user has a shared domain module (<domain>.py) and wants an OpenEnv variant, never duplicate domain logic into the framework folder ā wrap it.
Archetypes (pick the one matching the task)
| Archetype | Hallmarks |
|---|
| Pure-Python game | Deterministic, single @mcp.tool, text-only observations. Reward computed externally from the trajectory. |
| Stateful sandbox | E2B / browser / DB, multiple tools mutating session state, MCPEnvironment per session. |
| Vision / computer-use | Screenshots returned as MCP image content blocks (fastmcp.utilities.types.Image), 19-tool action surface modelled on Anthropic's computer_20251124, optional custom Gradio UI mounted at /web. |
Recommended file layout
The user picks the actual paths. The canonical shape:
<env_dir>/openenv/
āāā pyproject.toml # openenv-core[core] + e2b-* + fastmcp + uvicorn + gradio
āāā __init__.py
āāā models.py # Pydantic State / typed action / observation models
āāā Dockerfile # multi-stage from ghcr.io/meta-pytorch/openenv-base
āāā openenv.yaml # spec_version 1, name, runtime, app, port
āāā server/
ā āāā __init__.py
ā āāā app.py # create_app(EnvCls, CallToolAction, CallToolObservation, env_name=...)
ā āāā <env>_environment.py # MCPEnvironment subclass with @mcp.tool methods
āāā rollout.py # MCPToolClient drives the server; auto-discovers tools
āāā README.md # one-page; with HF frontmatter if deploying to Spaces
Implementation order (one continuous pass)
1. Pydantic state model ā models.py
Subclass openenv.core.env_server.types.State. Add per-episode fields you'll mutate (step_count, last_output, sandbox/session ids, anything you want to inspect later).
For visual envs, add last_screenshot_b64. Don't store huge blobs unless you need them in state ā use metadata on observations instead.
2. The MCPEnvironment ā server/<name>_environment.py
class MyEnv(MCPEnvironment):
SUPPORTS_CONCURRENT_SESSIONS = True
def __init__(self):
mcp = FastMCP("my_env")
@mcp.tool
def my_tool(arg: int) -> str: ...
super().__init__(mcp)
Key contracts:
- Dual-import pattern. Inside
server/, write try: from ..models import X; except ImportError: from models import X. Relative imports work inside the repo (PYTHONPATH=src:envs); flat imports work in Docker (/app/env). Same applies to sibling modules like e2b_sandbox.py.
- Tool methods are
@mcp.tool decorated functions inside __init__. They close over self and read/write env state. Don't try to put @mcp.tool on instance methods ā FastMCP introspects free functions.
- For images, use
fastmcp.utilities.types.Image: return Image(data=png_bytes, format="png"). The model receives an MCP image content block. Returning a base64 string in text means the model is blind.
- Lifecycle hooks:
reset(seed=None, episode_id=None, **kwargs) returns an Observation; step(action, timeout_s=None, **kwargs) is inherited from MCPEnvironment for tool dispatch ā only override if you need pre/post hooks (e.g. step-counter increment, terminate signal handling).
3. The FastAPI app ā server/app.py
import os
from openenv.core.env_server.http_server import create_app
from openenv.core.env_server.mcp_types import CallToolAction, CallToolObservation
try:
from .my_environment import MyEnv
from .gradio_ui import my_ui_builder
except ImportError:
from server.my_environment import MyEnv
from server.gradio_ui import my_ui_builder
def _custom_gradio_builder(*args, **kwargs):
return my_ui_builder(env_factory=MyEnv)
os.environ["ENABLE_WEB_INTERFACE"] = "true"
app = create_app(
MyEnv, CallToolAction, CallToolObservation,
env_name="my_env",
max_concurrent_envs=int(os.getenv("MAX_CONCURRENT_ENVS", "4")),
gradio_builder=_custom_gradio_builder,
)
Pass the class to create_app, not an instantiated env.
4. Custom Gradio UI (optional, computer-use-style envs benefit)
server/gradio_ui.py defines my_ui_builder(env_factory) that returns a gr.Blocks. Mounted at /web (set base_path: /web in the HF Space frontmatter). For computer-use envs, the canonical pattern includes an iframe panel showing the E2B stream URL alongside text controls ā but any gr.Blocks layout works.
5. The rollout ā rollout.py
Use openenv.core.mcp_client.MCPToolClient. Discover tools, don't hardcode:
from openenv.core.mcp_client import MCPToolClient
with MCPToolClient(base_url=ENV_URL).sync() as env:
env.reset()
tools = env.list_tools()
Note: for image-returning tools, env.call_tool strips to result.data (which is None for image returns). Use env.step(CallToolAction(tool_name="screenshot", arguments={})) to get the full result dict, then read obs.result["content"][0]["data"] for the b64 image. Pattern:
def _call(env, name, **kwargs):
out = env.step(CallToolAction(tool_name=name, arguments=kwargs))
return out.observation.result or {}
def _b64_screenshot(env):
res = _call(env, "screenshot")
for c in res.get("content", []) or []:
if c.get("type") == "image" and c.get("data"):
return c["data"]
raise RuntimeError(f"screenshot returned no image: {res}")
For multimodal models (Qwen3-VL, GPT-4o), feed the latest screenshot as an image block in the user message every turn.
6. The Dockerfile
Use a multi-stage build:
FROM ghcr.io/meta-pytorch/openenv-base:latest (the official base ā already has FastAPI, MCP, Gradio).
uv sync twice (no-install-project, then with project) for cache friendliness.
- Healthcheck via
/health.
- Expose port 8000.
For HF Spaces, the canonical app_port is 8000 (not 7860 ā OpenEnv's pattern uses 8000). Set base_path: /web in the README frontmatter so Gradio mounts under that prefix.
7. The HF Space README frontmatter
---
title: My Env Server
emoji: š¤
colorFrom: blue
colorTo: purple
sdk: docker
pinned: false
app_port: 8000
base_path: /web
tags: [openenv, your-domain]
short_description: One-line summary
---
Validation gates
Before declaring done, all four must pass:
- In-repo import ā
PYTHONPATH=envs uv run python -c "from envs.<name>.openenv.server.<name>_environment import <Cls>"
- Local server ā
uv run uvicorn server.app:app --port 8000 then curl /health returns {"status":"healthy"} and /list_environments returns the env name.
- Tool discovery ā
MCPToolClient.list_tools() returns the expected list.
- Rollout ā
MAX_TURNS=3 uv run python rollout.py runs without errors.
Common gotchas (from real-world OpenEnv work)
KeyError: 'tools' from POST /list_tools ā OpenEnv doesn't expose /list_tools directly; MCPToolClient uses /step with a list-tools action under the hood. Always discover via the client.
- Screenshot returns
None ā env.call_tool("screenshot") returns only the structured data field. Use env.step(CallToolAction(...)) and read obs.result["content"].
address already in use ā common during local-dev iteration. Just pick a different --port.
ModuleNotFoundError in Docker but works locally ā missing dual-import pattern in server/app.py or server/<name>_environment.py.
Reference
references/architecture.md ā full architecture deep-dive (when needed)
Official documentation