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.

Copy link
Contributor

Choose a reason for hiding this comment

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

See if we can rename the file name and not use types.py. It starts conflicting usually.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah good point


# Reset the environment
print("1. Resetting environment...")
result = client.reset()
Copy link
Contributor

Choose a reason for hiding this comment

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

is resetting a pre-req to list_tools call? Or in other words, does reset() semantically have any impact on the list_tools?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Keep in mind that list_tools() should be removed since instead we are just going to allow env.step(ListToolsAction()). Apart from that, hmmm I think that we may still want you to call .reset() first to initialize the env, in case some of your tools require state (e.g. a driving sim)

Contains the list of available tools with their schemas.
"""

tools: List[Dict[str, Any]] = field(default_factory=list)
Copy link
Contributor

Choose a reason for hiding this comment

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

@Darktex - can we use a strongly types schema(openAPI or similar) like how MCP list/tools does.
Each ToolSpec is defined as:

{
  name: string;          // Unique identifier for the tool
  description?: string;  // Human-readable description
  inputSchema: {         // JSON Schema for the tool's parameters
    type: "object",
    properties: { ... }  // Tool-specific parameters
  }
}

And this API returns a list of the above typed object:

[ToolSpec]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good callout

metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass(kw_only=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

I would strongly recommend that we create typed objects for tools and use that in client as well as the server:

@dataclass
class Tool:
    name: str
    description: str
    input_schema: Dict[str, Any]
    output_schema: Dict[str, Any]

Contains the result of calling a tool, or an error if the call failed.
"""

result: Optional[Any] = None
Copy link
Contributor

Choose a reason for hiding this comment

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

All fields must be required for a CallToolObservation. Lets remove optional

We should be specific about error here: error coming from the tool execution needs to be part of the result object and not the error field. In other words, we should assume that tool will bundle the error in the Result and we will return the same as is in the result field.

For any other errors including but not limited to - invalid args, transport level issue, etc.., we can have an error field as we have here. Error field could also be more expanded:

class Error:
   error_type: enum
   error_str: description
   ....

from fastmcp import Client

self.mcp_server = mcp_server
self.mcp_client = Client(mcp_server)
Copy link
Contributor

Choose a reason for hiding this comment

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

I would imagine that we will need to support multiple mcp servers and hence, multiple mcp clients in one environment. Are you planning on adding the routing support in a separate PR for the same?

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 Correct! For now, let's do a single, local MCP server

* Initial plan

* Address code review feedback from copilot-pull-request-reviewer

Co-authored-by: Darktex <[email protected]>

* Address secondary code review comments

Co-authored-by: Darktex <[email protected]>

* Remove list_tools and echo_message convenience methods per review feedback

Co-authored-by: Darktex <[email protected]>

* Address code review feedback: add import examples and use isinstance

Co-authored-by: Darktex <[email protected]>

* Remove unnecessary isinstance check in echo_mcp_demo.py

Co-authored-by: Darktex <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: Darktex <[email protected]>
@Darktex
Copy link
Contributor Author

Darktex commented Dec 1, 2025

@copilot Can you send me a PR implementing Pankit's comments?

Copy link

Copilot AI commented Dec 1, 2025

@Darktex I've opened a new pull request, #230, 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.

3 participants