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 MCP tools/list JSON-RPC call)
  • Tool invocation via CallToolAction (mirrors the MCP tools/call JSON-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's step() method
  • ListToolsObservation / CallToolObservation – Observations returned to the agent
  • Tool, ToolError, ToolErrorType – Tool specifications and error handling
  • WSMCPMessage / 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() – Returns True if the environment supports code execution
  • get_callables() – Returns MCP tools as plain Python callables
  • execute_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 MCPEnvironment in src/openenv/core/env_server/mcp_environment.py, which automatically routes ListToolsAction and CallToolAction through 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() and execute_code(), enabling direct execution without JSON-RPC overhead.
  • The echo_mcp_demo.py example in examples/echo_mcp_demo.py showcases 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →