Skip to content

Commit 91121cc

Browse files
committed
add frozen lake server to package
1 parent 1d681bf commit 91121cc

File tree

3 files changed

+314
-0
lines changed

3 files changed

+314
-0
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""
2+
FrozenLake Environment Adapter
3+
4+
This adapter implements the EnvironmentAdapter interface for FrozenLake environments,
5+
enabling integration with the MCP-Gym framework.
6+
"""
7+
8+
from typing import Any, Dict, Optional, Tuple
9+
10+
from gymnasium.envs.toy_text.frozen_lake import FrozenLakeEnv, generate_random_map
11+
12+
from eval_protocol.mcp.adapter import EnvironmentAdapter
13+
14+
15+
class FrozenLakeAdapter(EnvironmentAdapter):
16+
"""FrozenLake adapter for MCP-Gym framework."""
17+
18+
ACTION_NAMES = ["LEFT", "DOWN", "RIGHT", "UP"]
19+
20+
def create_environment(self, config: Optional[Dict[str, Any]] = None) -> FrozenLakeEnv:
21+
"""
22+
Create FrozenLake environment.
23+
24+
Args:
25+
config: Configuration dictionary with optional 'map_name' and 'seed'
26+
27+
Returns:
28+
FrozenLake environment instance
29+
"""
30+
print(f"🔍 FrozenLakeAdapter.create_environment: config: {config}")
31+
config = config or {}
32+
33+
# Determine grid size from config
34+
grid_size = 4
35+
if "map_name" in config:
36+
if "8x8" in config["map_name"]:
37+
grid_size = 8
38+
39+
# Generate random map if seed is provided
40+
seed = config.get("seed")
41+
print(f"🔍 FrozenLakeAdapter.create_environment: extracted seed: {seed} (type: {type(seed)})")
42+
print(f"🔍 FrozenLakeAdapter.create_environment: grid_size: {grid_size}")
43+
44+
if seed is not None:
45+
print(f"🔍 FrozenLakeAdapter.create_environment: Generating map with seed {seed}")
46+
desc = generate_random_map(size=grid_size, p=0.8, seed=seed)
47+
print(f"🔍 FrozenLakeAdapter.create_environment: Generated map desc: {desc}")
48+
else:
49+
print("🔍 FrozenLakeAdapter.create_environment: Generating map without seed")
50+
desc = generate_random_map(size=grid_size, p=0.8)
51+
print(f"🔍 FrozenLakeAdapter.create_environment: Generated map desc: {desc}")
52+
53+
env = FrozenLakeEnv(desc=desc, is_slippery=False, render_mode="ansi")
54+
print("🔍 FrozenLakeAdapter.create_environment: Created FrozenLakeEnv")
55+
return env
56+
57+
def create_environment_with_seed(
58+
self, config: Optional[Dict[str, Any]] = None, seed: Optional[int] = None
59+
) -> Tuple[FrozenLakeEnv, int, Dict[str, Any]]:
60+
"""
61+
Create FrozenLake environment with seed and return initial state.
62+
63+
Args:
64+
config: Configuration dictionary
65+
seed: Seed for reproducible environments
66+
67+
Returns:
68+
Tuple of (environment, initial_observation, initial_info)
69+
"""
70+
print(f"🔍 FrozenLakeAdapter.create_environment_with_seed: config: {config}, seed: {seed}")
71+
config = config or {}
72+
73+
# Add seed to config for environment creation
74+
env_config = {**config, "seed": seed}
75+
print(f"🔍 FrozenLakeAdapter.create_environment_with_seed: env_config: {env_config}")
76+
77+
env = self.create_environment(env_config)
78+
print(f"🔍 FrozenLakeAdapter.create_environment_with_seed: created env, calling reset with seed: {seed}")
79+
obs, info = env.reset(seed=seed)
80+
print(f"🔍 FrozenLakeAdapter.create_environment_with_seed: reset returned obs: {obs}, info: {info}")
81+
82+
return env, obs, info
83+
84+
def reset_environment(self, env: FrozenLakeEnv, seed: Optional[int] = None) -> Tuple[int, Dict[str, Any]]:
85+
"""
86+
Reset environment.
87+
88+
Args:
89+
env: Environment instance
90+
seed: Optional seed for reset
91+
92+
Returns:
93+
Tuple of (observation, info)
94+
"""
95+
return env.reset(seed=seed)
96+
97+
def step_environment(self, env: FrozenLakeEnv, action: int) -> Tuple[int, float, bool, bool, Dict[str, Any]]:
98+
"""
99+
Execute environment step.
100+
101+
Args:
102+
env: Environment instance
103+
action: Action index
104+
105+
Returns:
106+
Tuple of (observation, reward, terminated, truncated, info)
107+
"""
108+
return env.step(action)
109+
110+
def close_environment(self, env: FrozenLakeEnv) -> None:
111+
"""
112+
Close environment.
113+
114+
Args:
115+
env: Environment instance
116+
"""
117+
# FrozenLake doesn't need explicit cleanup
118+
pass
119+
120+
def parse_action(self, action_str: str) -> int:
121+
"""
122+
Parse action string to integer.
123+
124+
Args:
125+
action_str: Action string (LEFT, DOWN, RIGHT, UP)
126+
127+
Returns:
128+
Action index
129+
130+
Raises:
131+
ValueError: If action is invalid
132+
"""
133+
action_str = action_str.strip().upper()
134+
if action_str not in self.ACTION_NAMES:
135+
raise ValueError(f"Invalid action '{action_str}'. Valid actions: {self.ACTION_NAMES}")
136+
return self.ACTION_NAMES.index(action_str)
137+
138+
def format_observation(self, observation: int) -> int:
139+
"""
140+
Format observation for JSON serialization.
141+
142+
Args:
143+
observation: Raw observation from environment
144+
145+
Returns:
146+
Formatted observation
147+
"""
148+
return int(observation)
149+
150+
def get_default_config(self) -> Dict[str, Any]:
151+
"""
152+
Get default configuration.
153+
154+
Returns:
155+
Default configuration dictionary
156+
"""
157+
return {
158+
"map_name": "4x4",
159+
"is_slippery": False,
160+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
FrozenLake MCP-Gym Implementation
3+
4+
This module implements the north star vision for MCP-Gym environments,
5+
providing a clean, simple implementation of FrozenLake using the McpGym base class.
6+
7+
Key Features:
8+
- Multi-session support with session-based control plane state
9+
- Data plane: Tool responses contain only observations
10+
- Control plane: Server-side state management keyed by session ID
11+
- Rollout system can query control plane state for termination logic
12+
13+
Example usage:
14+
from frozen_lake_mcp import FrozenLakeMcp
15+
16+
server = FrozenLakeMcp(seed=42)
17+
server.run()
18+
"""
19+
20+
from typing import Any, Dict, Optional
21+
22+
from frozen_lake_adapter import FrozenLakeAdapter
23+
from mcp.server.fastmcp import Context
24+
25+
from eval_protocol.mcp import McpGym
26+
27+
28+
class FrozenLakeMcp(McpGym):
29+
"""
30+
FrozenLake MCP-Gym environment implementing the north star vision.
31+
32+
This demonstrates the clean, simple API for MCP-Gym environments:
33+
- Inherit from McpGym (which inherits from GymProductionServer)
34+
- Use proper EnvironmentAdapter pattern
35+
- Register tools with @self.mcp.tool() decorator
36+
- Compatible with CondaServerProcessManager
37+
- Multi-session support with session-based control plane state
38+
"""
39+
40+
def __init__(self, seed: Optional[int] = None, **kwargs):
41+
"""Initialize FrozenLake MCP-Gym environment."""
42+
adapter = FrozenLakeAdapter()
43+
super().__init__("FrozenLake-v1", adapter, seed, **kwargs)
44+
45+
# Multi-session support is now handled by the base class
46+
47+
# Session management methods are now handled by the base class
48+
49+
def _register_tools(self):
50+
"""Register domain-specific MCP tools."""
51+
52+
@self.mcp.tool(
53+
name="lake_move",
54+
description="Move on the frozen lake. Actions: LEFT, DOWN, RIGHT, UP. "
55+
"Returns only observation data; control plane state managed server-side.",
56+
)
57+
def lake_move(action: str, ctx: Context) -> Dict[str, Any]:
58+
"""
59+
Move in the FrozenLake environment.
60+
61+
Args:
62+
action: Direction to move (LEFT, DOWN, RIGHT, UP)
63+
ctx: MCP context (proper FastMCP context)
64+
65+
Returns:
66+
Dictionary with observation data ONLY (data plane).
67+
Control plane state managed server-side per session.
68+
"""
69+
# Validate action
70+
if not action or not isinstance(action, str):
71+
raise ValueError(
72+
f"Invalid action parameter: '{action}'. "
73+
f"Must be a non-empty string. Valid actions: LEFT, DOWN, RIGHT, UP"
74+
)
75+
76+
action = action.strip().upper()
77+
78+
# Parse action
79+
try:
80+
action_int = self.adapter.parse_action(action)
81+
except ValueError as e:
82+
raise ValueError(str(e))
83+
84+
# Get session ID and session data
85+
session_id = self._get_session_id(ctx)
86+
session_data = self._get_or_create_session(ctx)
87+
88+
# Execute environment step using base class method
89+
observation_data = self._execute_session_environment_step(session_id, action_int)
90+
observation_data["action"] = action
91+
92+
# Log move (no control plane data in logs)
93+
print(f"🎮 Session {session_id[:16]}...: {action} → position {session_data['obs']}")
94+
95+
return observation_data
96+
97+
def format_observation(self, obs: int, env: Any) -> Dict[str, Any]:
98+
"""Format observation for MCP response (data plane only)."""
99+
return {
100+
"position": int(obs),
101+
"grid": env.render(),
102+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python3
2+
"""
3+
FrozenLake MCP-Gym Server
4+
5+
This script launches the FrozenLake MCP-Gym server using the proper MCP-Gym framework.
6+
Compatible with CondaServerProcessManager for isolated execution.
7+
8+
Usage:
9+
python server.py --port 9004 --seed 42
10+
"""
11+
12+
import argparse
13+
import os
14+
import sys
15+
from pathlib import Path
16+
17+
# Add root directory to path so we can import eval_protocol
18+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
19+
20+
from frozen_lake_mcp import FrozenLakeMcp
21+
22+
23+
def main():
24+
"""Run the FrozenLake MCP server."""
25+
parser = argparse.ArgumentParser(description="FrozenLake MCP Server")
26+
parser.add_argument(
27+
"--transport",
28+
choices=["streamable-http", "stdio"],
29+
default="streamable-http",
30+
help="Transport protocol to use",
31+
)
32+
parser.add_argument("--port", type=int, default=8000, help="Port for HTTP transport")
33+
parser.add_argument("--seed", type=int, default=None, help="Seed for the environment")
34+
35+
args = parser.parse_args()
36+
37+
# Set environment variable for HTTP port (required by FastMCP)
38+
if args.transport == "streamable-http":
39+
os.environ["PORT"] = str(args.port)
40+
41+
# Create and run server
42+
server = FrozenLakeMcp(seed=args.seed)
43+
44+
print(f"🚀 Starting FrozenLake MCP server on port {args.port}")
45+
print(f"🌱 Seed: {args.seed}")
46+
print(f"📡 Transport: {args.transport}")
47+
48+
server.run(transport=args.transport)
49+
50+
51+
if __name__ == "__main__":
52+
main()

0 commit comments

Comments
 (0)