Skip to content

Commit ad8b9d1

Browse files
committed
Merge branch 'main' of github.com:reward-protocol/python-sdk
2 parents ec011aa + cf55dfe commit ad8b9d1

29 files changed

Lines changed: 2545 additions & 816 deletions

examples/frozen_lake_mcp/frozen_lake_adapter.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77

88
from typing import Any, Dict, Optional, Tuple
99

10-
from gymnasium.envs.toy_text import FrozenLakeEnv
11-
from gymnasium.envs.toy_text.frozen_lake import generate_random_map
10+
from gymnasium.envs.toy_text.frozen_lake import generate_random_map, FrozenLakeEnv
1211

1312
from reward_kit.mcp.adapter import EnvironmentAdapter
1413

examples/frozen_lake_mcp/frozen_lake_mcp.py

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -99,30 +99,10 @@ def lake_move(action: str, ctx: Context) -> Dict[str, Any]:
9999

100100
return observation_data
101101

102-
103102
@staticmethod
104103
def format_observation(obs: int, env: Any) -> Dict[str, Any]:
105104
"""Format observation for MCP response (data plane only)."""
106105
return {
107106
"position": int(obs),
108107
"grid": env.render(),
109108
}
110-
111-
112-
# Example usage and testing
113-
# if __name__ == "__main__":
114-
# # Test the FrozenLake MCP-Gym environment
115-
# print("Creating FrozenLake MCP-Gym server...")
116-
# server = FrozenLakeMcp(seed=42)
117-
#
118-
# print("Server created successfully!")
119-
# print(f"Environment adapter: {server.adapter.__class__.__name__}")
120-
# print("\n🎛️ Multi-session control plane features:")
121-
# print(" - Session-based environment isolation")
122-
# print(" - Server-side control plane state management")
123-
# print(" - get_control_plane_state tool for rollout system")
124-
# print(" - Data plane tools return observations only")
125-
#
126-
# # Run the server
127-
# print("\nStarting MCP server...")
128-
# server.run()
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
"""
2+
FrozenLake MCP-Gym Implementation - Simplified Version
3+
4+
This module implements the north star vision for MCP-Gym environments,
5+
providing a clean, simple implementation of FrozenLake WITHOUT the adapter pattern.
6+
7+
Key Features:
8+
- Strict data/control plane separation
9+
- Data plane: Tool responses contain only observations
10+
- Control plane: Rewards/termination available via MCP resources (control://reward, control://status)
11+
- Direct environment handling (no adapter layer)
12+
- <50% complexity of adapter-based version
13+
14+
Example usage:
15+
from frozen_lake_mcp_simplified import FrozenLakeMcpSimplified
16+
17+
server = FrozenLakeMcpSimplified(seed=42)
18+
server.run()
19+
"""
20+
21+
import json
22+
from typing import Any, Dict, Optional
23+
24+
from gymnasium.envs.toy_text import FrozenLakeEnv
25+
from gymnasium.envs.toy_text.frozen_lake import generate_random_map
26+
from mcp.server.fastmcp import Context, FastMCP
27+
28+
29+
class FrozenLakeMcpSimplified:
30+
"""
31+
Simplified FrozenLake MCP-Gym environment implementing the north star vision.
32+
33+
This demonstrates the clean, simple API for MCP-Gym environments WITHOUT adapters:
34+
- Direct environment handling (no adapter pattern)
35+
- Built-in action parsing and observation formatting
36+
- Register tools with @self.mcp.tool() decorator
37+
- Compatible with CondaServerProcessManager
38+
- Strict data/control plane separation via MCP resources
39+
"""
40+
41+
ACTION_NAMES = ["LEFT", "DOWN", "RIGHT", "UP"]
42+
43+
def __init__(self, seed: Optional[int] = None, grid_size: int = 4):
44+
"""Initialize simplified FrozenLake MCP-Gym environment."""
45+
self.grid_size = grid_size
46+
47+
# Create environment directly
48+
self.env = self._create_environment(seed)
49+
self.obs, _info = self.env.reset(seed=seed)
50+
51+
# Initialize control plane state
52+
self.control_plane_state = {
53+
"reward": 0.0,
54+
"terminated": False,
55+
"truncated": False,
56+
"info": {},
57+
"step_count": 0,
58+
"total_reward": 0.0,
59+
}
60+
61+
# Create FastMCP server
62+
import os
63+
64+
self.mcp = FastMCP(
65+
"FrozenLake-Simplified-v1",
66+
host="0.0.0.0",
67+
port=int(os.environ.get("PORT", 8000)),
68+
)
69+
70+
# Register resources and tools
71+
self._register_control_plane_resources()
72+
self._register_standard_resources()
73+
self._register_tools()
74+
75+
def _create_environment(self, seed: Optional[int] = None) -> FrozenLakeEnv:
76+
"""Create FrozenLake environment directly."""
77+
if seed is not None:
78+
desc = generate_random_map(size=self.grid_size, p=0.8, seed=seed)
79+
else:
80+
desc = generate_random_map(size=self.grid_size, p=0.8)
81+
82+
return FrozenLakeEnv(desc=desc, is_slippery=False, render_mode="ansi")
83+
84+
def _register_control_plane_resources(self):
85+
"""Register MCP resources for control plane information."""
86+
87+
@self.mcp.resource("control://reward")
88+
def current_reward() -> str:
89+
"""Provide current reward information via MCP resource."""
90+
return json.dumps(
91+
{
92+
"reward": self.control_plane_state["reward"],
93+
"step_count": self.control_plane_state["step_count"],
94+
}
95+
)
96+
97+
@self.mcp.resource("control://status")
98+
def current_status() -> str:
99+
"""Provide current episode status via MCP resource."""
100+
return json.dumps(
101+
{
102+
"terminated": self.control_plane_state["terminated"],
103+
"truncated": self.control_plane_state["truncated"],
104+
"step_count": self.control_plane_state["step_count"],
105+
"total_reward": self.control_plane_state["total_reward"],
106+
}
107+
)
108+
109+
@self.mcp.resource("control://info")
110+
def current_info() -> str:
111+
"""Provide current environment info via MCP resource."""
112+
return json.dumps(self.control_plane_state["info"])
113+
114+
def _register_standard_resources(self):
115+
"""Register standard MCP resources."""
116+
117+
@self.mcp.resource("game://initial_state")
118+
def initial_state() -> str:
119+
"""Provide initial game state as MCP resource."""
120+
return json.dumps(self._format_observation(self.obs))
121+
122+
def _register_tools(self):
123+
"""Register domain-specific MCP tools."""
124+
125+
@self.mcp.tool(
126+
name="lake_move",
127+
description="Move on the frozen lake. Actions: LEFT, DOWN, RIGHT, UP. "
128+
"Check control://reward and control://status resources for rewards and termination.",
129+
)
130+
def lake_move(action: str, ctx: Context) -> Dict[str, Any]:
131+
"""
132+
Move in the FrozenLake environment.
133+
134+
Args:
135+
action: Direction to move (LEFT, DOWN, RIGHT, UP)
136+
ctx: MCP context (proper FastMCP context)
137+
138+
Returns:
139+
Dictionary with observation data ONLY (data plane).
140+
Rewards and termination info available via control plane resources.
141+
"""
142+
# Validate action
143+
if not action or not isinstance(action, str):
144+
raise ValueError(
145+
f"Invalid action parameter: '{action}'. "
146+
f"Must be a non-empty string. Valid actions: LEFT, DOWN, RIGHT, UP"
147+
)
148+
149+
action = action.strip().upper()
150+
151+
# Parse action (directly without adapter)
152+
if action not in self.ACTION_NAMES:
153+
raise ValueError(
154+
f"Invalid action '{action}'. Valid actions: {self.ACTION_NAMES}"
155+
)
156+
action_int = self.ACTION_NAMES.index(action)
157+
158+
# Execute environment step with control plane separation
159+
observation_data = self._execute_environment_step(action_int)
160+
161+
# Add the action to the response for context
162+
observation_data["action"] = action
163+
164+
# Log the result (control plane state is already logged in _execute_environment_step)
165+
if (
166+
self.control_plane_state["terminated"]
167+
or self.control_plane_state["truncated"]
168+
):
169+
status = (
170+
"🏆 GOAL!" if self.control_plane_state["reward"] > 0 else "💀 HOLE!"
171+
)
172+
print(f"🎮 Game ended: {status}")
173+
else:
174+
print(f"🎮 {action} → position {self.obs}")
175+
176+
# Return ONLY data plane information (no rewards/termination)
177+
return observation_data
178+
179+
def _execute_environment_step(self, action_int: int) -> Dict[str, Any]:
180+
"""
181+
Execute environment step and update control plane (directly without adapter).
182+
183+
Args:
184+
action_int: Parsed action integer
185+
186+
Returns:
187+
Data plane response (observation only, no rewards)
188+
"""
189+
# Execute environment step directly
190+
obs, reward, terminated, truncated, info = self.env.step(action_int)
191+
192+
# Update global observation state
193+
self.obs = obs
194+
195+
# Update control plane (separate from data plane)
196+
self._update_control_plane(reward, terminated, truncated, info)
197+
198+
# Return ONLY data plane information (no rewards/termination)
199+
return self._format_observation(obs)
200+
201+
def _update_control_plane(
202+
self, reward: float, terminated: bool, truncated: bool, info: Dict[str, Any]
203+
):
204+
"""
205+
Update control plane state after environment step.
206+
207+
Args:
208+
reward: Reward from environment step
209+
terminated: Whether episode terminated
210+
truncated: Whether episode truncated
211+
info: Info dictionary from environment
212+
"""
213+
self.control_plane_state["reward"] = reward
214+
self.control_plane_state["terminated"] = terminated
215+
self.control_plane_state["truncated"] = truncated
216+
self.control_plane_state["info"] = info
217+
self.control_plane_state["step_count"] += 1
218+
self.control_plane_state["total_reward"] += reward
219+
220+
# Log control plane update (for debugging)
221+
print(
222+
f"🎛️ Control plane updated: reward={reward}, terminated={terminated}, step={self.control_plane_state['step_count']}"
223+
)
224+
225+
def _format_observation(self, obs: int) -> Dict[str, Any]:
226+
"""Format observation for MCP response (data plane only, directly without adapter)."""
227+
return {
228+
"position": int(obs),
229+
"grid": self.env.render(),
230+
}
231+
232+
def run(self, transport: str = "streamable-http", **kwargs):
233+
"""Run the MCP server."""
234+
print(f"🚀 {self.mcp.name} Simplified Server Starting...")
235+
print(f"📡 Transport: {transport}")
236+
print("🎯 MCP Pattern: Resources for initial state, tools for actions")
237+
print("🔗 Initial state resource: game://initial_state")
238+
print("\n🎛️ Control plane resources available:")
239+
print(" - control://reward (current reward and step count)")
240+
print(" - control://status (termination status and total reward)")
241+
print(" - control://info (environment info)")
242+
print(f"\n📊 Environment: {self.grid_size}x{self.grid_size} FrozenLake")
243+
print(f"📍 Initial position: {self.obs}")
244+
245+
# Run the server
246+
self.mcp.run(transport=transport, **kwargs)
247+
248+
249+
# Example usage and testing
250+
if __name__ == "__main__":
251+
# Test the simplified FrozenLake MCP-Gym environment
252+
print("Creating simplified FrozenLake MCP-Gym server...")
253+
server = FrozenLakeMcpSimplified(seed=42)
254+
255+
print("Server created successfully!")
256+
print(f"Initial observation: {server.obs}")
257+
print("🎯 NO ADAPTER PATTERN - direct environment handling")
258+
259+
# Run the server
260+
print("\nStarting MCP server...")
261+
server.run()

examples/frozen_lake_mcp/test_multi_session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ async def test_static_policy_only():
122122
policy.initialize_conversations(
123123
n_envs=2,
124124
system_prompts=["Test system prompt 1", "Test system prompt 2"],
125-
initial_user_prompts=["Test user prompt 1", "Test user prompt 2"],
125+
user_prompts=["Test user prompt 1", "Test user prompt 2"],
126126
)
127127

128128
# Test action generation

0 commit comments

Comments
 (0)