Summary
Tools registered with @self.tool(mode="production") on an MCPEnvironment are not reachable through the production MCP transport served by HTTPEnvServer. With register_routes(app, mode="production"):
tools/list omits production-mode tools
tools/call fails with Unknown tool
MCPToolClient cannot list or call them
This affects both the HTTP /mcp JSON-RPC path and the WebSocket MCP path.
Reproduction
class DualModeEnv(MCPEnvironment):
SUPPORTS_CONCURRENT_SESSIONS = True
def __init__(self):
mcp = FastMCP("demo")
super().__init__(mcp)
@mcp.tool
def shared_tool(x: int) -> int:
return x * 10
@self.tool(mode="production")
def production_tool(msg: str) -> str:
return f"PROD_RESULT: {msg}"
@self.tool(mode="simulation")
def simulation_tool(msg: str) -> str:
return f"SIM_RESULT: {msg}"
server = HTTPEnvServer(
env=DualModeEnv,
action_cls=CallToolAction,
observation_cls=CallToolObservation,
)
server.register_routes(app, mode="production")
Using FastAPI TestClient, create a session via POST /mcp, then:
tools/list returns only shared_tool; production_tool is missing.
tools/call:
{"jsonrpc": "2.0", "method": "tools/call", "id": 103,
"params": {"name": "production_tool", "arguments": {"msg": "hello-prod"}}}
returns:
{"jsonrpc": "2.0", "id": 103,
"error": {"code": -32603, "message": "Unknown tool: 'production_tool'", "data": null}}
MCPToolClient:
list_tools() -> ['shared_tool']
call_tool('production_tool', ...) -> RuntimeError: Tool 'production_tool' failed: Unknown tool
The WebSocket MCP path behaves the same. The failure is deterministic and reproduces on main at 7e591317fa85dab4991b6869ac33c0d303b645df.
Root cause
There are two related problems.
1. mcp_handler bypasses the mode-aware registry.
MCPEnvironment.tool(mode=...) stores mode-specific tools in _mode_tools / _mode_tool_schemas, not on the underlying FastMCP server. HTTPEnvServer.mcp_handler handles tools/list and tools/call by delegating to mcp_client.list_tools() and mcp_client.call_tool(...), which only see the FastMCP registry. Mode-aware tools are never consulted.
2. The server mode is not propagated to environment instances.
register_routes(app, mode="production") receives the mode, but _create_session() does not apply it to new environments, so getattr(env, "_mode", None) is None instead of "production". The existing mode-aware resolution therefore cannot pick the right implementation.
Relevant code:
src/openenv/core/env_server/mcp_environment.py (mode-aware registration, _mode_tools, _mode_tool_schemas, tool resolution)
src/openenv/core/env_server/http_server.py (HTTPEnvServer.mcp_handler, register_routes, _create_session)
Expected behavior
With register_routes(app, mode="production"):
tools/list returns mode-agnostic tools plus production-mode tools, without duplicates
tools/call dispatches production-mode tools to their production implementation
- simulation-only tools do not appear in the production tool list
- the same holds over HTTP
/mcp, WebSocket MCP, and MCPToolClient
Suggested fix direction
- Pass the server mode into environments created by
_create_session() so env._mode is set correctly.
- In
mcp_handler, build tools/list from the FastMCP tools plus the mode-appropriate entries of _mode_tool_schemas, deduplicated by name.
- In
mcp_handler, resolve tools/call through the environment's existing mode-aware dispatch when the tool is in the mode registry.
- Reuse the existing
MCPEnvironment mode-aware logic instead of adding a second registration mechanism.
No change to the public @self.tool(mode=...) API is needed.
Test coverage gap
Current mode-aware tests call step() directly after manually setting the environment mode. Current production MCP tests mostly cover mode-agnostic FastMCP tools. Nothing exercises the full path:
HTTP /mcp -> JSON-RPC -> HTTPEnvServer.mcp_handler -> MCPEnvironment -> mode-aware registry
Proposed regression tests
Using a real TestClient against /mcp with mode="production":
- Create an MCP session.
- Call
tools/list and assert that both the shared tool and the production tool are present, and the simulation tool is absent.
- Call
tools/call on the production tool and assert it returns the production result.
Add the same scenario through MCPToolClient (and ideally WebSocket MCP) to cover the client-to-server path.
Scope
Limited to the interaction between mode-aware MCP tools, HTTPEnvServer, and the production MCP transport.
Related
Summary
Tools registered with
@self.tool(mode="production")on anMCPEnvironmentare not reachable through the production MCP transport served byHTTPEnvServer. Withregister_routes(app, mode="production"):tools/listomits production-mode toolstools/callfails withUnknown toolMCPToolClientcannot list or call themThis affects both the HTTP
/mcpJSON-RPC path and the WebSocket MCP path.Reproduction
Using FastAPI
TestClient, create a session viaPOST /mcp, then:tools/listreturns onlyshared_tool;production_toolis missing.tools/call:{"jsonrpc": "2.0", "method": "tools/call", "id": 103, "params": {"name": "production_tool", "arguments": {"msg": "hello-prod"}}}returns:
{"jsonrpc": "2.0", "id": 103, "error": {"code": -32603, "message": "Unknown tool: 'production_tool'", "data": null}}MCPToolClient:The WebSocket MCP path behaves the same. The failure is deterministic and reproduces on
mainat7e591317fa85dab4991b6869ac33c0d303b645df.Root cause
There are two related problems.
1.
mcp_handlerbypasses the mode-aware registry.MCPEnvironment.tool(mode=...)stores mode-specific tools in_mode_tools/_mode_tool_schemas, not on the underlying FastMCP server.HTTPEnvServer.mcp_handlerhandlestools/listandtools/callby delegating tomcp_client.list_tools()andmcp_client.call_tool(...), which only see the FastMCP registry. Mode-aware tools are never consulted.2. The server mode is not propagated to environment instances.
register_routes(app, mode="production")receives the mode, but_create_session()does not apply it to new environments, sogetattr(env, "_mode", None)isNoneinstead of"production". The existing mode-aware resolution therefore cannot pick the right implementation.Relevant code:
src/openenv/core/env_server/mcp_environment.py(mode-aware registration,_mode_tools,_mode_tool_schemas, tool resolution)src/openenv/core/env_server/http_server.py(HTTPEnvServer.mcp_handler,register_routes,_create_session)Expected behavior
With
register_routes(app, mode="production"):tools/listreturns mode-agnostic tools plus production-mode tools, without duplicatestools/calldispatches production-mode tools to their production implementation/mcp, WebSocket MCP, andMCPToolClientSuggested fix direction
_create_session()soenv._modeis set correctly.mcp_handler, buildtools/listfrom the FastMCP tools plus the mode-appropriate entries of_mode_tool_schemas, deduplicated by name.mcp_handler, resolvetools/callthrough the environment's existing mode-aware dispatch when the tool is in the mode registry.MCPEnvironmentmode-aware logic instead of adding a second registration mechanism.No change to the public
@self.tool(mode=...)API is needed.Test coverage gap
Current mode-aware tests call
step()directly after manually setting the environment mode. Current production MCP tests mostly cover mode-agnostic FastMCP tools. Nothing exercises the full path:Proposed regression tests
Using a real
TestClientagainst/mcpwithmode="production":tools/listand assert that both the shared tool and the production tool are present, and the simulation tool is absent.tools/callon the production tool and assert it returns the production result.Add the same scenario through
MCPToolClient(and ideally WebSocket MCP) to cover the client-to-server path.Scope
Limited to the interaction between mode-aware MCP tools,
HTTPEnvServer, and the production MCP transport.Related