Setting up MCP Integration for Agent Tool Discovery in OpenEnv: A Complete Guide
To set up MCP integration in OpenEnv, subclass MCPEnvironment from src/openenv/core/env_server/mcp_environment.py, register tools via the FastMCP server, and route actions using ListToolsAction and CallToolAction for discovery and invocation.
The Model Context Protocol (MCP) provides a standardized interface for AI agents to discover and invoke tools within the OpenEnv framework. This protocol layers on top of the classic Gym-style step() API, enabling consistent tool discovery across production and simulation environments while maintaining support for direct Python code execution.
What Is MCP Integration in OpenEnv?
OpenEnv exposes MCP as a first-class interface that bridges FastMCP servers with the environment's action-observation loop. According to the OpenEnv source code, this integration supports four key capabilities:
- Tool discovery via
ListToolsAction(mirrors the MCPtools/listJSON-RPC call) - Tool invocation via
CallToolAction(mirrors the MCPtools/callJSON-RPC call) - Mode-aware registration where tools can be restricted to production, simulation, or both modes
- Code mode execution where agents can write Python code that calls MCP tools as ordinary callables
The core implementation resides in src/openenv/core/env_server/mcp_environment.py, with type definitions in src/openenv/core/env_server/mcp_types.py.
Core Architecture
The MCPEnvironment Base Class
MCPEnvironment subclasses the standard Environment class and implements a dual API that handles both MCP-specific actions and traditional Gym-style actions. In src/openenv/core/env_server/mcp_environment.py, the class provides:
MCP Server Integration: The __init__ method accepts a FastMCP server instance, validates tool names against reserved keywords (reset, step, state, close), and creates a Client for RPC calls.
Tool Registration: The tool decorator allows mode-specific registration. Tools without a mode are registered directly on the FastMCP server, while mode-qualified tools are stored for conditional exposure.
Step Routing: The step() method detects ListToolsAction and CallToolAction, forwarding them to _handle_list_tools and _handle_call_tool respectively. All other actions delegate to the abstract _step_impl method that subclasses must implement.
Async Support: Async helpers _async_handle_list_tools and _async_handle_call_tool support both synchronous step (via run_async_safely) and WebSocket-based step_async operations.
Session Management: The mcp_session async context manager guarantees a stable FastMCP client session across HTTP and WebSocket requests.
MCP Types and JSON-RPC Models
All MCP payloads use strongly typed Pydantic models defined in src/openenv/core/env_server/mcp_types.py:
ListToolsAction/CallToolsAction– Actions sent to the environment'sstep()methodListToolsObservation/CallToolObservation– Observations returned to the agentTool,ToolError,ToolErrorType– Tool specifications and error handlingWSMCPMessage/WSMCPResponse– WebSocket message envelopes for direct MCP access
Setting Up MCP in Your Environment
Follow these steps to add MCP-driven tool discovery to any OpenEnv environment.
Step 1: Create a FastMCP Server
Initialize a FastMCP instance that will manage your tool registry:
from fastmcp import FastMCP
mcp = FastMCP("my-server")
Step 2: Register Mode-Aware Tools
Use the @mcp.tool() decorator to register functions. You can optionally specify modes to control tool availability:
@mcp.tool()
def my_tool(x: int, y: int) -> int:
"""Add two integers."""
return x + y
@mcp.tool(mode="production") # Only available in production mode
def sensitive_operation(data: str) -> str:
"""Process sensitive data."""
return data.upper()
Step 3: Subclass MCPEnvironment
Import the base class and implement _step_impl for non-MCP actions:
from openenv.core.env_server.mcp_environment import MCPEnvironment
class MyEnv(MCPEnvironment):
def __init__(self):
super().__init__(mcp) # Validates tool names automatically
def _step_impl(self, action, **kwargs):
# Handle standard Gym actions here
raise NotImplementedError("Implement environment-specific logic")
Step 4: Handle Tool Discovery and Invocation
Use the unified API to discover and call tools:
from openenv.core.env_server.mcp_types import ListToolsAction, CallToolAction
env = MyEnv()
env.reset()
# Discover available tools
obs = env.step(ListToolsAction())
print("Available tools:", [t.name for t in obs.tools])
# Invoke a specific tool
obs = env.step(CallToolAction(
tool_name="my_tool",
arguments={"x": 2, "y": 3}
))
print("Result:", obs.result.data)
Code Mode and Direct Execution
OpenEnv supports code mode for agents that generate and execute Python code directly. The MCPEnvironment class exposes three key methods in src/openenv/core/env_server/mcp_environment.py:
supports_code_mode()– ReturnsTrueif the environment supports code executionget_callables()– Returns MCP tools as plain Python callablesexecute_code()– Runs generated code with tools available in the namespace
This allows agents to bypass the JSON-RPC round-trip and call tools directly as functions:
# Tools appear as ordinary callables in code mode
callables = env.get_callables()
result = callables["my_tool"](2, 3) # Direct invocation
Complete Working Example
The EchoEnvironment in envs/echo_env/server/echo_environment.py demonstrates a concrete implementation. It registers two simple tools:
@mcp.tool()
def echo_message(message: str) -> str:
"""Return the message unchanged."""
return message
@mcp.tool()
def echo_with_length(message: str) -> str:
"""Return the message together with its character count."""
return f"{message} ({len(message)} characters)"
The demo script in examples/echo_mcp_demo.py shows the canonical SIM mode workflow:
from echo_env.server.echo_environment import EchoEnvironment
from openenv.core.env_server.mcp_types import ListToolsAction, CallToolAction
env = EchoEnvironment()
env.reset()
# 1️⃣ Discover tools
obs = env.step(ListToolsAction())
print("Available tools:", [t.name for t in obs.tools])
# 2️⃣ Call a tool
obs = env.step(CallToolAction(
tool_name="echo_message",
arguments={"message": "Hello MCP!"}
))
print("Result:", obs.result.data)
# 3️⃣ Handle errors gracefully
obs = env.step(CallToolAction(tool_name="nonexistent_tool", arguments={}))
print("Error:", obs.error)
Running this demo (PYTHONPATH=src:envs uv run python examples/echo_mcp_demo.py) demonstrates tool discovery, successful invocation, and error handling for non-existent tools.
Summary
- MCP integration is provided by
MCPEnvironmentinsrc/openenv/core/env_server/mcp_environment.py, which automatically routesListToolsActionandCallToolActionthrough a FastMCP server. - Tool names are validated against reserved keywords (
reset,step,state,close) during initialization. - Mode-aware registration allows tools to be restricted to production, simulation, or both environments.
- The step-based interface provides a single entry point for both standard Gym actions and MCP tool usage, ensuring consistent logging and reproducibility.
- Code mode exposes MCP tools as Python callables via
get_callables()andexecute_code(), enabling direct execution without JSON-RPC overhead. - The
echo_mcp_demo.pyexample inexamples/echo_mcp_demo.pyshowcases the complete workflow: reset → list tools → call tools → handle errors.
Frequently Asked Questions
How does OpenEnv validate MCP tool names?
According to the source code in src/openenv/core/env_server/mcp_environment.py, the MCPEnvironment.__init__ method validates tool names against a reserved list including reset, step, state, and close. This prevents naming collisions with standard Gym methods and ensures the environment API remains unambiguous.
Can I use MCP tools in both synchronous and asynchronous contexts?
Yes. The MCPEnvironment class provides async helpers _async_handle_list_tools and _async_handle_call_tool that support both synchronous step() (via run_async_safely) and WebSocket-based step_async() operations. The mcp_session async context manager ensures stable FastMCP client sessions across HTTP and WebSocket requests.
What is the difference between mode-specific and mode-agnostic tools?
Mode-agnostic tools (registered with @mcp.tool() without a mode argument) are available in all contexts. Mode-specific tools use mode="production" or mode="simulation" and are only exposed when the environment runs in that specific mode. This allows you to restrict sensitive operations to production environments while keeping simulation tools isolated.
How do I handle errors when calling MCP tools in OpenEnv?
Error handling is built into the MCP types defined in src/openenv/core/env_server/mcp_types.py. When calling a non-existent tool or providing invalid arguments, CallToolAction returns a CallToolObservation containing a ToolError object with error_type and message fields. Your agent should check obs.error after each step(CallToolAction(...)) call to handle failures gracefully.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →