Skip to content

Conversation

@Darktex
Copy link
Contributor

@Darktex Darktex commented Nov 25, 2025

As part of implementing RFC 003, I'm adding support for MCP.

At Meta, we typically work with Stacked PRs as a way to stay sane and allow for easier review.

For this reason, this is just the first PR in the stack, but it is already adding the MCP functionality.

Implementation Phases:

  • PR 1 (Current): Core MCP infrastructure + echo_env conversion
  • PR 2 (Future): Migrate remaining environments
  • PR 3 (Future): CodeAct environment with MCP integration
  • PR 4 (Future): Fix double marshaling with callable introspection

@meta-cla meta-cla bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 25, 2025
@@ -0,0 +1,329 @@
# RFC-003 Implementation Journal
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was written by Claude. Keeping this here while I implement, but I will remove it when the stack lands.



class EchoEnv(HTTPEnvClient[EchoAction, EchoObservation]):
class EchoEnv(HTTPEnvClient[CallToolAction, Observation]):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now I followed the HTTPEnvClient convention, but to me this is the weakest part of OpenEnv as we require that people write boilerplate that we can generate for them.

I will tackle this when we go into supporting ToolCall and CodeAct at the same time, as that is a natural fit for generating this boilerplate on the fly.

Raises:
ValueError: If MCP client not configured or action type invalid
"""
from .types import CallToolObservation, ListToolsObservation
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude this, but me no like. Will fix.

observation = await loop.run_in_executor(
self._executor, self.env.step, action
)
# Handle MCP actions asynchronously (don't use thread pool for async operations)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pankit-eng this is where I need your eyes the most. Does this make sense?

Copilot finished reviewing on behalf of Darktex November 25, 2025 02:22
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements the core MCP (Model Context Protocol) infrastructure for OpenEnv as the first phase of RFC-003. It introduces a new MCPEnvironment base class that allows environments to expose tools via the MCP protocol, and converts the echo_env as a reference implementation. The changes enable both gym-style orchestration through HTTP endpoints and direct MCP JSON-RPC access for production agents.

Key Changes:

  • Added MCP action types (ListToolsAction, CallToolAction) and corresponding observations to the core type system
  • Implemented MCPEnvironment base class that handles MCP client/server communication boilerplate
  • Extended HTTP server with /mcp JSON-RPC endpoint for direct tool access alongside existing /step endpoint
  • Refactored echo_env to use MCP pattern, replacing custom EchoAction/EchoObservation with MCP tools

Reviewed changes

Copilot reviewed 15 out of 17 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
src/core/env_server/types.py Added MCP action and observation types (ListToolsAction, CallToolAction, ListToolsObservation, CallToolObservation)
src/core/env_server/mcp_environment.py New base class providing MCP infrastructure for environments
src/core/env_server/http_server.py Added /mcp JSON-RPC endpoint and MCP action routing in /step handler
src/core/env_server/__init__.py Exported new MCP types and MCPEnvironment class
src/core/pyproject.toml Added mcp and fastmcp dependencies
src/envs/echo_env/server/mcp_server.py New MCP tool definitions for echo environment
src/envs/echo_env/server/echo_environment.py Refactored to extend MCPEnvironment instead of custom implementation
src/envs/echo_env/server/app.py Updated to use base Action/Observation types for MCP support
src/envs/echo_env/models.py Removed custom EchoAction and EchoObservation (deprecated in favor of MCP)
src/envs/echo_env/client.py Updated to handle MCP actions and parse MCP observations
src/envs/echo_env/__init__.py Removed exports of deprecated custom action/observation types
tests/core/mcp/test_mcp.py New test file for MCP client/server integration
examples/test_mcp_integration.py Integration test script demonstrating MCP usage
examples/echo_mcp_demo.py Demo script showing MCP tool interaction
rfcs/RFC-003-implementation-journal.md Implementation journal documenting decisions and progress
Comments suppressed due to low confidence (1)

src/core/env_server/http_server.py:17

  • Import of 'json' is not used.
import json

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

Returns:
JSON-RPC 2.0 response
"""
if self.env.mcp_client is None:
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /mcp endpoint assumes self.env.mcp_client exists, but not all Environment instances will have this attribute. This will raise an AttributeError for non-MCP environments. Add a hasattr(self.env, 'mcp_client') check or check if the env is an instance of MCPEnvironment first.

Suggested change
if self.env.mcp_client is None:
if not hasattr(self.env, "mcp_client") or self.env.mcp_client is None:

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pankit-eng this is good catch by copilot. Do you think we should just add that line?

Comment on lines +53 to +58
>>> tools = client.list_tools()
>>> print(tools) # [{"name": "echo_message", ...}]
>>>
>>> # Call echo_message tool
>>> result = client.echo_message("Hello!")
>>> print(result["echoed_message"]) # "Hello!"
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring mentions list_tools() and echo_message() methods that are not implemented in the EchoEnv class. Either implement these convenience methods or update the documentation to reflect the actual API (using step() with ListToolsAction and CallToolAction).

Copilot uses AI. Check for mistakes.

# List available tools
print("2. Listing available tools...")
tools = client.list_tools()
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list_tools() method is not implemented in the EchoEnv client class (see client.py). This call will fail with an AttributeError. Either implement the method in the client or use the step() method with a ListToolsAction.

Copilot uses AI. Check for mistakes.

# Call echo_message tool using convenience method
print("3. Calling echo_message tool...")
result = client.echo_message("Hello from MCP!")
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The echo_message() method is not implemented in the EchoEnv client class (see client.py). This call will fail with an AttributeError. Either implement the method in the client or use the step() method with a CallToolAction.

Copilot uses AI. Check for mistakes.
"id": None,
}

body = await request.json()
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /mcp endpoint lacks error handling for malformed JSON. If request.json() fails (e.g., invalid JSON in request body), it will raise an exception. Consider wrapping the await request.json() call in a try-except block and returning a JSON-RPC error with code -32700 ("Parse error") for invalid JSON.

Copilot uses AI. Check for mistakes.
Comment on lines +243 to +244
return CallToolAction(
tool_name=action_data["tool_name"],
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessing action_data["tool_name"] will raise a KeyError if the tool_name field is missing from the request. Use action_data.get("tool_name") with an appropriate error message if it's None, or validate that the field exists before accessing it.

Suggested change
return CallToolAction(
tool_name=action_data["tool_name"],
tool_name = action_data.get("tool_name")
if tool_name is None:
raise ValueError("Missing required field 'tool_name' for CallToolAction")
return CallToolAction(
tool_name=tool_name,

Copilot uses AI. Check for mistakes.

try:
# Reuse MCP client from environment (avoids creating duplicate client)
async with self.env.mcp_client:
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MCP client is being used within an async with block in the /mcp endpoint (line 168) and also in _handle_mcp_action (mcp_environment.py line 133). This could lead to issues if the client doesn't support being entered multiple times concurrently. Consider whether the client should be managed at the environment level or if each endpoint should manage its own context.

Copilot uses AI. Check for mistakes.

self.mcp_server = mcp_server
self.mcp_client = Client(mcp_server)
super().__init__(mcp_client=self.mcp_client)
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Environment.__init__ base class doesn't accept an mcp_client parameter. This call will fail with a TypeError. The base class __init__ signature is def __init__(self, transform: Transform | None = None) (see interfaces.py line 95).

Either:

  1. Remove the mcp_client=self.mcp_client argument here, or
  2. Update the Environment.__init__ signature to accept mcp_client as an optional parameter
Suggested change
super().__init__(mcp_client=self.mcp_client)
super().__init__()

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +25
from core.env_server.types import (
CallToolAction,
CallToolObservation,
ListToolsAction,
ListToolsObservation,
Observation,
State,
)
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'ListToolsAction' is not used.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +36
from openenv_core.env_server.types import (
CallToolAction,
CallToolObservation,
ListToolsAction,
ListToolsObservation,
Observation,
State,
)
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import of 'ListToolsAction' is not used.

Copilot uses AI. Check for mistakes.
@Darktex
Copy link
Contributor Author

Darktex commented Nov 25, 2025

@copilot I accept all your suggestions. Can you implement these changes?

Copy link

Copilot AI commented Nov 25, 2025

@Darktex I've opened a new pull request, #226, to work on those changes. Once the pull request is ready, I'll request review from you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants