Creating your own environment
Write an environment as an AgentEnvEnvironment, with the tools an agent calls and the data plane the framework uses
You write an environment as a subclass of AgentEnvEnvironment, from the
agentenv-framework-protocol package. Decorators on its methods tell the framework what each method
is for. This server, the one these pages follow, holds an inbox that an agent can search and a sent
folder that records what the agent sends:
import json
from pathlib import Path
from agentenv_protocol import (
AgentEnvEnvironment,
DataPart,
FilePart,
add_data,
environment_card,
get_data,
reset_data,
tool,
)
@environment_card(name="email")
class EmailEnv(AgentEnvEnvironment):
def __init__(self) -> None:
self.inbox: list[dict] = []
self.sent: list[dict] = []
@reset_data
async def _reset(self) -> None:
self.inbox.clear()
self.sent.clear()
@add_data
async def _add(self, parts: list) -> None:
for part in parts:
if isinstance(part, DataPart):
self.inbox.extend(part.data.get("inbox", []))
self.sent.extend(part.data.get("sent", []))
elif isinstance(part, FilePart):
seed = json.loads(Path(part.file.uri.removeprefix("file://")).read_text())
self.inbox.extend(seed.get("inbox", []))
self.sent.extend(seed.get("sent", []))
@get_data
async def _state(self) -> list:
return [DataPart(data={"inbox": self.inbox, "sent": self.sent})]
@tool(name="{environment_name}_search")
async def search(self, query: str) -> dict:
"""Find inbox emails whose subject or body contains the query."""
q = query.lower()
return {"emails": [e for e in self.inbox if q in (e["subject"] + " " + e["body"]).lower()]}
@tool()
async def send(self, to: str, subject: str, body: str) -> dict:
"""Send an email."""
self.sent.append({"to": to, "subject": subject, "body": body})
return {"sent": len(self.sent)}
if __name__ == "__main__":
EmailEnv().serve()Tools
The two @tool methods are what the agent can do. Each one becomes an MCP tool: its docstring
becomes the tool's description, and its type hints become the tool's input schema. send
therefore takes three required strings, to, subject and body.
A tool's name is its method's name, unless you pass one to @tool. search passes
{environment_name}_search, and when the server starts, the placeholder becomes the environment's
name, so the tool is email_search. send passes none, so its tool is send. The prefix is
optional. It exists because of the gateway, a proxy the
framework runs, by default, in front of every env instance: when Composing
environments puts a contacts server behind the same gateway, prefixes
keep the two servers' tools apart.
The data plane
The @reset_data, @add_data and @get_data methods form the data plane: a second endpoint,
/agentenv, that the framework uses to control the environment's state. It speaks JSON-RPC, where
each request is a JSON object that names a method. Each decorator answers one method: data/reset
empties the inbox and the sent folder, data/add loads emails into them, inline or from a file,
and data/get reads both back.
The data plane is not an MCP tool, so the agent is never offered it.
EmailEnv is one small environment, and a task often needs more than one.
Composing environments writes a second one and combines the two,
and Deploying your environment runs EmailEnv in a sandbox and
calls its data plane.
Last updated on