# Setting up MCP Integration for Agent Tool Discovery in OpenEnv: A Complete Guide

> Master MCP integration for agent tool discovery in OpenEnv. Follow our guide to subclass MCPEnvironment, register tools with FastMCP, and efficiently route actions for seamless discovery and invocation.

- Repository: [Hugging Face/OpenEnv](https://github.com/huggingface/OpenEnv)
- Tags: how-to-guide
- Published: 2026-06-14

---

**To set up MCP integration in OpenEnv, subclass `MCPEnvironment` from [`src/openenv/core/env_server/mcp_environment.py`](https://github.com/huggingface/OpenEnv/blob/main/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`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_server/mcp_environment.py), with type definitions in [`src/openenv/core/env_server/mcp_types.py`](https://github.com/huggingface/OpenEnv/blob/main/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`](https://github.com/huggingface/OpenEnv/blob/main/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`](https://github.com/huggingface/OpenEnv/blob/main/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:

```python
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:

```python
@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:

```python
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:

```python
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`](https://github.com/huggingface/OpenEnv/blob/main/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:

```python

# 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`](https://github.com/huggingface/OpenEnv/blob/main/envs/echo_env/server/echo_environment.py) demonstrates a concrete implementation. It registers two simple tools:

```python
@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`](https://github.com/huggingface/OpenEnv/blob/main/examples/echo_mcp_demo.py) shows the canonical SIM mode workflow:

```python
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`](https://github.com/huggingface/OpenEnv/blob/main/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`](https://github.com/huggingface/OpenEnv/blob/main/echo_mcp_demo.py) example in [`examples/echo_mcp_demo.py`](https://github.com/huggingface/OpenEnv/blob/main/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`](https://github.com/huggingface/OpenEnv/blob/main/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`](https://github.com/huggingface/OpenEnv/blob/main/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.