agentenv-framework-protocol 0.1.403__tar.gz → 0.1.404__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/PKG-INFO +72 -5
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/README.md +71 -4
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/pyproject.toml +1 -1
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/__init__.py +6 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/agent_env_environment.py +206 -7
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/__init__.py +2 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/driver.py +5 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/extensions.py +1 -1
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/keys.py +2 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/skill.py +30 -7
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_agent_env_environment.py +13 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_driver.py +18 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_tool_access.py +25 -3
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_tool_annotations.py +155 -1
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/.gitignore +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/LICENSE +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/NOTICE +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/THIRD_PARTY_NOTICES.md +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/advanced_agent.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/basic_agent.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/custom_extensions_agent.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/multimodal_agent.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/reference_model.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/examples/streaming_agent.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/_triggers.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/extensions.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/framework.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/proxy.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/registry.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/staging.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/tasks/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/a2a_agent/tasks/v1.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/client.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/_declare.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/_load.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/app.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/app_runtime.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/apps.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/card.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/checks.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/__init__.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/__main__.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/checks.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/client.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/fixture.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/mp4.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/conformance/report.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/errors.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/frames.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/launch.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/linux.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/operations.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/platform.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/queue.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/recording.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/results.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/runtime.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/serve.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/serve_app.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/session.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/settings.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/spaces.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/computer/tools.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/errors.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/manifest.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/preflight.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/tool_access.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/transfers.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/src/agentenv_protocol/types.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/computer_fake.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/probe_fake.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_agent.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_agent_examples.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_object_transfers.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_peer_agents.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_proxy.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_staging.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_a2a_trajectory_log.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_client.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_app_server.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_conformance.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_conformance_checks.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_conformance_recording.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_dataplane.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_declare.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_extensions.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_frames.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_queue.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_recording.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_serve.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_computer_spaces.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_data_objects.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_errors.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_extension_methods.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_intake.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_manifest.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_preflight.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_tool_client.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_tool_context.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_tool_context_fastmcp.py +0 -0
- {agentenv_framework_protocol-0.1.403 → agentenv_framework_protocol-0.1.404}/tests/test_tool_context_injection.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentenv-framework-protocol
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.404
|
|
4
4
|
Summary: Open data-plane protocol and server SDK for agent environments.
|
|
5
5
|
Project-URL: Homepage, https://www.agentenvframework.com
|
|
6
6
|
Project-URL: Documentation, https://www.agentenvframework.com/docs
|
|
@@ -296,6 +296,69 @@ at the gateway.
|
|
|
296
296
|
|
|
297
297
|
Where the binding lives depends on the app. On mcp's own FastMCP it wraps the tool manager's dispatch; on the standalone `fastmcp` package it joins the app's middleware chain as the outermost layer, so a handler's own middleware runs under it too. An app with neither seam mounts unbound, with a warning, unless a tool declares a `ToolContext` parameter or the handler defines `on_tool_call`: then the mount is refused rather than serving an empty caller.
|
|
298
298
|
|
|
299
|
+
### Tools served on another server's behalf
|
|
300
|
+
|
|
301
|
+
An environment can serve tools another server defines, each with that server's own schema:
|
|
302
|
+
`forwarded_tools()` returns `ForwardedTool(tool, call, annotations, output_schema)` entries, where
|
|
303
|
+
`tool` is an `EnvironmentTool` carrying the other server's `inputSchema`, and `call` takes the
|
|
304
|
+
call's arguments as keywords, untouched, and returns that server's `CallToolResult`, which reaches
|
|
305
|
+
the caller as it is. An `output_schema` is listed as the tool's `outputSchema` and isn't checked
|
|
306
|
+
here, so `call` answers every result, its errors too, in a shape that schema admits. They're listed
|
|
307
|
+
on the card beside the `@tool` methods, chosen by `served_tools` and under tool access like them;
|
|
308
|
+
one named like another tool is refused. They need mcp's FastMCP. The cua-driver driver serves
|
|
309
|
+
cua-driver's own tools this way.
|
|
310
|
+
|
|
311
|
+
### Resources
|
|
312
|
+
|
|
313
|
+
Mark a method that serves MCP resources with `@resource`; an env may mark several, and each read
|
|
314
|
+
calls the method, which returns text or bytes. The URI decides what's served:
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
@environment_card(name="orders")
|
|
318
|
+
class Orders(AgentEnvEnvironment):
|
|
319
|
+
@resource("docs://orders/refund-policy", mime_type="text/markdown")
|
|
320
|
+
def refund_policy(self) -> str:
|
|
321
|
+
"""When a refund is allowed and who approves it."""
|
|
322
|
+
return POLICY.read_text()
|
|
323
|
+
|
|
324
|
+
@resource("orders://{order_id}")
|
|
325
|
+
async def order(self, order_id: str) -> str:
|
|
326
|
+
return json.dumps(await self.db.order(order_id))
|
|
327
|
+
|
|
328
|
+
@resource("skill://upstream/{file}", listed=lambda env: UPSTREAM["resources"])
|
|
329
|
+
async def upstream_file(self, file: str) -> str:
|
|
330
|
+
return await read_upstream(f"skill://upstream/{file}")
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
- A fixed URI is one resource. Its name defaults to the method's name and its description to the
|
|
334
|
+
docstring.
|
|
335
|
+
- A template such as `orders://{order_id}` is an MCP resource template; the method takes its parts.
|
|
336
|
+
- A template with `listed` serves the resources `listed` returns, each as MCP lists one (`uri`,
|
|
337
|
+
`name`, `description`, `mimeType`), and only those. Each is read through the method with its
|
|
338
|
+
URI's parts. That's how an env serves another server's resources from a list: the cua-driver
|
|
339
|
+
driver serves cua-driver's skill files this way.
|
|
340
|
+
|
|
341
|
+
A URI served twice, a listed URI that doesn't match its template, and a method that doesn't take
|
|
342
|
+
exactly the template's parts are refused. Resources need mcp's FastMCP.
|
|
343
|
+
|
|
344
|
+
### Instructions at connect
|
|
345
|
+
|
|
346
|
+
An MCP server may answer `initialize` with `instructions`: how to use it, which a client may put in
|
|
347
|
+
the agent's context, as Claude Code does. Mark the plain method that returns them with
|
|
348
|
+
`@server_instructions`:
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
@environment_card(name="orders")
|
|
352
|
+
class Orders(AgentEnvEnvironment):
|
|
353
|
+
@server_instructions
|
|
354
|
+
def how_to(self) -> str:
|
|
355
|
+
return "Look an order up with get_order before refunding it; refunds can't be undone."
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
They're read once, when the SDK builds the app (`create_app`, `serve`, `create_fastmcp_app`), and
|
|
359
|
+
every session gets the same text. An env has at most one such method. An app you build yourself
|
|
360
|
+
and pass to `mount` keeps its own instructions: set them when you construct it.
|
|
361
|
+
|
|
299
362
|
### Getting part of the data
|
|
300
363
|
|
|
301
364
|
A `@get_data` handler that takes a `paths` parameter returns only the data under the paths a caller
|
|
@@ -318,9 +381,11 @@ def guide(self):
|
|
|
318
381
|
The card then declares `urn:agentenv:skill/v1` (`SKILL_EXTENSION_URI`), whose `get {name}`
|
|
319
382
|
answers `{name, description, skill_md}`, the shape `skill-config/v1` takes, with the frontmatter
|
|
320
383
|
written under the name the caller gives; a name that isn't lowercase letters, digits and single
|
|
321
|
-
hyphens, up to 64, is refused `invalid_argument`.
|
|
322
|
-
|
|
323
|
-
|
|
384
|
+
hyphens, up to 64, is refused `invalid_argument`. A skill with other files beside its SKILL.md
|
|
385
|
+
returns them as a third value, `{relative path: text}`, and `get` answers them as `files`.
|
|
386
|
+
agent-env's `add_skills` step installs it on an agent through `env_ids`, named after the env's
|
|
387
|
+
environment name, a skill with files as a bundle of them; for an env behind a gateway, whose card
|
|
388
|
+
nests its servers' cards, it installs each server's skill under that server's name.
|
|
324
389
|
|
|
325
390
|
### Uploading an export through a grant
|
|
326
391
|
|
|
@@ -482,7 +547,9 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
|
|
|
482
547
|
features. The driver serves its skill through `skill/v1` (an `@agent_skill` handler on the base
|
|
483
548
|
class): a SKILL.md written from the computer as it booted (its platform, session user and home,
|
|
484
549
|
the live screen size, what its input takes, its OS's shortcuts) and ending with the driver
|
|
485
|
-
class's `notes`, markdown for what its card can't say.
|
|
550
|
+
class's `notes`, markdown for what its card can't say. A driver whose software ships a skill of
|
|
551
|
+
its own returns it from `bundled_skill()` (its description, body and other files), and the
|
|
552
|
+
computer skill leads with it, the computer's part following.
|
|
486
553
|
- **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
|
|
487
554
|
capture, the driver's own `ready_checks()`, and one per app on the computer, asked of the app's
|
|
488
555
|
own server (see Computer apps); tools refuse `device_unavailable` until they pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
|
|
@@ -240,6 +240,69 @@ at the gateway.
|
|
|
240
240
|
|
|
241
241
|
Where the binding lives depends on the app. On mcp's own FastMCP it wraps the tool manager's dispatch; on the standalone `fastmcp` package it joins the app's middleware chain as the outermost layer, so a handler's own middleware runs under it too. An app with neither seam mounts unbound, with a warning, unless a tool declares a `ToolContext` parameter or the handler defines `on_tool_call`: then the mount is refused rather than serving an empty caller.
|
|
242
242
|
|
|
243
|
+
### Tools served on another server's behalf
|
|
244
|
+
|
|
245
|
+
An environment can serve tools another server defines, each with that server's own schema:
|
|
246
|
+
`forwarded_tools()` returns `ForwardedTool(tool, call, annotations, output_schema)` entries, where
|
|
247
|
+
`tool` is an `EnvironmentTool` carrying the other server's `inputSchema`, and `call` takes the
|
|
248
|
+
call's arguments as keywords, untouched, and returns that server's `CallToolResult`, which reaches
|
|
249
|
+
the caller as it is. An `output_schema` is listed as the tool's `outputSchema` and isn't checked
|
|
250
|
+
here, so `call` answers every result, its errors too, in a shape that schema admits. They're listed
|
|
251
|
+
on the card beside the `@tool` methods, chosen by `served_tools` and under tool access like them;
|
|
252
|
+
one named like another tool is refused. They need mcp's FastMCP. The cua-driver driver serves
|
|
253
|
+
cua-driver's own tools this way.
|
|
254
|
+
|
|
255
|
+
### Resources
|
|
256
|
+
|
|
257
|
+
Mark a method that serves MCP resources with `@resource`; an env may mark several, and each read
|
|
258
|
+
calls the method, which returns text or bytes. The URI decides what's served:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
@environment_card(name="orders")
|
|
262
|
+
class Orders(AgentEnvEnvironment):
|
|
263
|
+
@resource("docs://orders/refund-policy", mime_type="text/markdown")
|
|
264
|
+
def refund_policy(self) -> str:
|
|
265
|
+
"""When a refund is allowed and who approves it."""
|
|
266
|
+
return POLICY.read_text()
|
|
267
|
+
|
|
268
|
+
@resource("orders://{order_id}")
|
|
269
|
+
async def order(self, order_id: str) -> str:
|
|
270
|
+
return json.dumps(await self.db.order(order_id))
|
|
271
|
+
|
|
272
|
+
@resource("skill://upstream/{file}", listed=lambda env: UPSTREAM["resources"])
|
|
273
|
+
async def upstream_file(self, file: str) -> str:
|
|
274
|
+
return await read_upstream(f"skill://upstream/{file}")
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
- A fixed URI is one resource. Its name defaults to the method's name and its description to the
|
|
278
|
+
docstring.
|
|
279
|
+
- A template such as `orders://{order_id}` is an MCP resource template; the method takes its parts.
|
|
280
|
+
- A template with `listed` serves the resources `listed` returns, each as MCP lists one (`uri`,
|
|
281
|
+
`name`, `description`, `mimeType`), and only those. Each is read through the method with its
|
|
282
|
+
URI's parts. That's how an env serves another server's resources from a list: the cua-driver
|
|
283
|
+
driver serves cua-driver's skill files this way.
|
|
284
|
+
|
|
285
|
+
A URI served twice, a listed URI that doesn't match its template, and a method that doesn't take
|
|
286
|
+
exactly the template's parts are refused. Resources need mcp's FastMCP.
|
|
287
|
+
|
|
288
|
+
### Instructions at connect
|
|
289
|
+
|
|
290
|
+
An MCP server may answer `initialize` with `instructions`: how to use it, which a client may put in
|
|
291
|
+
the agent's context, as Claude Code does. Mark the plain method that returns them with
|
|
292
|
+
`@server_instructions`:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
@environment_card(name="orders")
|
|
296
|
+
class Orders(AgentEnvEnvironment):
|
|
297
|
+
@server_instructions
|
|
298
|
+
def how_to(self) -> str:
|
|
299
|
+
return "Look an order up with get_order before refunding it; refunds can't be undone."
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
They're read once, when the SDK builds the app (`create_app`, `serve`, `create_fastmcp_app`), and
|
|
303
|
+
every session gets the same text. An env has at most one such method. An app you build yourself
|
|
304
|
+
and pass to `mount` keeps its own instructions: set them when you construct it.
|
|
305
|
+
|
|
243
306
|
### Getting part of the data
|
|
244
307
|
|
|
245
308
|
A `@get_data` handler that takes a `paths` parameter returns only the data under the paths a caller
|
|
@@ -262,9 +325,11 @@ def guide(self):
|
|
|
262
325
|
The card then declares `urn:agentenv:skill/v1` (`SKILL_EXTENSION_URI`), whose `get {name}`
|
|
263
326
|
answers `{name, description, skill_md}`, the shape `skill-config/v1` takes, with the frontmatter
|
|
264
327
|
written under the name the caller gives; a name that isn't lowercase letters, digits and single
|
|
265
|
-
hyphens, up to 64, is refused `invalid_argument`.
|
|
266
|
-
|
|
267
|
-
|
|
328
|
+
hyphens, up to 64, is refused `invalid_argument`. A skill with other files beside its SKILL.md
|
|
329
|
+
returns them as a third value, `{relative path: text}`, and `get` answers them as `files`.
|
|
330
|
+
agent-env's `add_skills` step installs it on an agent through `env_ids`, named after the env's
|
|
331
|
+
environment name, a skill with files as a bundle of them; for an env behind a gateway, whose card
|
|
332
|
+
nests its servers' cards, it installs each server's skill under that server's name.
|
|
268
333
|
|
|
269
334
|
### Uploading an export through a grant
|
|
270
335
|
|
|
@@ -426,7 +491,9 @@ driver has them, through the `served_tools`, `served_extensions` and `served_ope
|
|
|
426
491
|
features. The driver serves its skill through `skill/v1` (an `@agent_skill` handler on the base
|
|
427
492
|
class): a SKILL.md written from the computer as it booted (its platform, session user and home,
|
|
428
493
|
the live screen size, what its input takes, its OS's shortcuts) and ending with the driver
|
|
429
|
-
class's `notes`, markdown for what its card can't say.
|
|
494
|
+
class's `notes`, markdown for what its card can't say. A driver whose software ships a skill of
|
|
495
|
+
its own returns it from `bundled_skill()` (its description, body and other files), and the
|
|
496
|
+
computer skill leads with it, the computer's part following.
|
|
430
497
|
- **Framework extensions.** `computer-ready/v1` (`status`: real checks of the display and a
|
|
431
498
|
capture, the driver's own `ready_checks()`, and one per app on the computer, asked of the app's
|
|
432
499
|
own server (see Computer apps); tools refuse `device_unavailable` until they pass once), `computer-display/v1`, `computer-spaces/v1` (the default space, written into the tool
|
|
@@ -11,8 +11,11 @@ from .agent_env_environment import (
|
|
|
11
11
|
AgentEnvEnvironment,
|
|
12
12
|
AgentEnvFastMCPApplication,
|
|
13
13
|
AgentEnvStarletteApplication,
|
|
14
|
+
ForwardedTool,
|
|
14
15
|
add_data,
|
|
15
16
|
agent_skill,
|
|
17
|
+
resource,
|
|
18
|
+
server_instructions,
|
|
16
19
|
create_fastmcp_app,
|
|
17
20
|
environment_card,
|
|
18
21
|
extension,
|
|
@@ -88,12 +91,15 @@ __all__ = [
|
|
|
88
91
|
"AgentEnvEnvironment",
|
|
89
92
|
"AgentEnvFastMCPApplication",
|
|
90
93
|
"AgentEnvStarletteApplication",
|
|
94
|
+
"ForwardedTool",
|
|
91
95
|
"create_fastmcp_app",
|
|
92
96
|
"environment_card",
|
|
93
97
|
"reset_data",
|
|
94
98
|
"add_data",
|
|
95
99
|
"get_data",
|
|
96
100
|
"agent_skill",
|
|
101
|
+
"resource",
|
|
102
|
+
"server_instructions",
|
|
97
103
|
"extension",
|
|
98
104
|
"tool",
|
|
99
105
|
"injecting",
|
|
@@ -38,6 +38,9 @@ _EXT_OP_ATTR = "_aee_ext_op"
|
|
|
38
38
|
_TOOL_ATTR = "_aee_tool"
|
|
39
39
|
_HIDDEN_BY_DEFAULT_ATTR = "_aee_hidden_by_default"
|
|
40
40
|
_TOOL_OPTIONS_ATTR = "_aee_tool_options"
|
|
41
|
+
_FORWARDED_ATTR = "_aee_forwarded"
|
|
42
|
+
_INSTRUCTIONS_ATTR = "_aee_instructions"
|
|
43
|
+
_RESOURCE_ATTR = "_aee_resource"
|
|
41
44
|
_CARD_CONFIG_ATTR = "_aee_card_config"
|
|
42
45
|
#: Set on the tool dispatch installed by ``_bind_tool_calls``; a guard downstream checks it at boot.
|
|
43
46
|
_TOOL_CONTEXT_BOUND_ATTR = "__agentenv_tool_context__"
|
|
@@ -128,21 +131,56 @@ def extension(uri: str, *, op: str | None = None, description: str | None = None
|
|
|
128
131
|
return deco
|
|
129
132
|
|
|
130
133
|
|
|
134
|
+
def resource(uri: str, *, name: str | None = None, description: str | None = None, mime_type: str = "text/plain",
|
|
135
|
+
listed: Callable[[Any], list[dict]] | None = None) -> Callable:
|
|
136
|
+
"""Mark a method that serves MCP resources, returning their text or bytes; an env may mark several, and each read
|
|
137
|
+
calls the method. A fixed ``uri`` is one resource. A template such as ``orders://{order_id}`` is an MCP resource
|
|
138
|
+
template, the method taking its parts, unless ``listed`` is given: it takes the env and returns the resources to
|
|
139
|
+
list, each as MCP lists one (``uri``, ``name``, ``description``, ``mimeType``), each read through the method with
|
|
140
|
+
its URI's parts. ``name`` defaults to the method's name and ``description`` to its docstring."""
|
|
141
|
+
parts = set(re.findall(r"{(\w+)}", uri))
|
|
142
|
+
|
|
143
|
+
def mark(fn: Callable) -> Callable:
|
|
144
|
+
params = set(list(inspect.signature(fn).parameters)[1:])
|
|
145
|
+
if params != parts:
|
|
146
|
+
raise ValueError(f"@resource {uri!r} needs a method taking {sorted(parts) or 'nothing'} besides self, "
|
|
147
|
+
f"not {sorted(params)}")
|
|
148
|
+
if listed is not None and not parts:
|
|
149
|
+
raise ValueError(f"@resource {uri!r} lists resources only from a template URI")
|
|
150
|
+
setattr(fn, _RESOURCE_ATTR, _ResourceMarker(uri, name, description, mime_type, listed))
|
|
151
|
+
return fn
|
|
152
|
+
|
|
153
|
+
return mark
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def server_instructions(fn: Callable) -> Callable:
|
|
157
|
+
"""Mark the plain method that returns what this env's MCP server tells an agent when it connects, the
|
|
158
|
+
``instructions`` of its initialize result, which clients may put in the agent's context. It's read once, when the
|
|
159
|
+
SDK builds the app (``create_app``, ``serve``, ``create_fastmcp_app``); an app passed to ``mount`` keeps its own."""
|
|
160
|
+
if inspect.iscoroutinefunction(fn):
|
|
161
|
+
raise TypeError(f"@server_instructions marks a plain method, not the coroutine {fn.__name__!r}")
|
|
162
|
+
setattr(fn, _INSTRUCTIONS_ATTR, True)
|
|
163
|
+
return fn
|
|
164
|
+
|
|
165
|
+
|
|
131
166
|
def agent_skill(fn: Callable) -> Callable:
|
|
132
|
-
"""Mark the method that writes this env's skill for agents, returning its description
|
|
133
|
-
is served under ``SKILL_EXTENSION_URI`` as ``get {name}``, which
|
|
167
|
+
"""Mark the method that writes this env's skill for agents, returning its description, its markdown body and,
|
|
168
|
+
optionally, its other files by relative path; it is served under ``SKILL_EXTENSION_URI`` as ``get {name}``, which
|
|
169
|
+
writes the SKILL.md under the name asked for and returns the files beside it."""
|
|
134
170
|
|
|
135
171
|
async def get(self, name: str) -> dict:
|
|
136
172
|
if len(name) > 64 or not _SKILL_NAME.fullmatch(name):
|
|
137
173
|
raise AgentEnvError("invalid_argument",
|
|
138
174
|
f"a skill's name is lowercase letters, digits and single hyphens, up to 64, not {name!r}")
|
|
139
|
-
description, body = await fn(self) if inspect.iscoroutinefunction(fn) else fn(self)
|
|
175
|
+
description, body, *files = await fn(self) if inspect.iscoroutinefunction(fn) else fn(self)
|
|
140
176
|
frontmatter = f"---\nname: {name}\ndescription: {json.dumps(description)}\n---\n"
|
|
141
|
-
return {"name": name, "description": description, "skill_md": f"{frontmatter}\n{body}"
|
|
177
|
+
return {"name": name, "description": description, "skill_md": f"{frontmatter}\n{body}",
|
|
178
|
+
**({"files": files[0]} if files and files[0] else {})}
|
|
142
179
|
|
|
143
180
|
get.__name__ = fn.__name__
|
|
144
181
|
return extension(SKILL_EXTENSION_URI, op="get", method="GET",
|
|
145
|
-
description="get returns a SKILL.md for agents under the name it's given
|
|
182
|
+
description="get returns a SKILL.md for agents under the name it's given, and the skill's other "
|
|
183
|
+
"files when it has any.")(get)
|
|
146
184
|
|
|
147
185
|
|
|
148
186
|
def _extension_slug(uri: str) -> str:
|
|
@@ -202,6 +240,36 @@ def tool(name: str | None = None, *, description: str | None = None, hidden_by_d
|
|
|
202
240
|
return deco
|
|
203
241
|
|
|
204
242
|
|
|
243
|
+
class ForwardedTool(NamedTuple):
|
|
244
|
+
"""A tool an environment serves on another server's behalf, as that server lists it: its descriptor, its own
|
|
245
|
+
schema included, its MCP ``annotations`` and its ``output_schema``; ``call`` takes the call's arguments as keywords
|
|
246
|
+
and returns the other server's ``CallToolResult``, which reaches the caller as it is."""
|
|
247
|
+
|
|
248
|
+
tool: EnvironmentTool
|
|
249
|
+
call: Callable[..., Any]
|
|
250
|
+
annotations: dict | None = None
|
|
251
|
+
output_schema: dict | None = None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class _ResourceMarker(NamedTuple):
|
|
255
|
+
uri: str
|
|
256
|
+
name: str | None
|
|
257
|
+
description: str | None
|
|
258
|
+
mime_type: str
|
|
259
|
+
listed: Callable[[Any], list[dict]] | None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class _ServedResource(NamedTuple):
|
|
263
|
+
"""One resource, or a resource template when ``template``, as it is registered: ``read`` returns its content."""
|
|
264
|
+
|
|
265
|
+
uri: str
|
|
266
|
+
name: str
|
|
267
|
+
description: str | None
|
|
268
|
+
mime_type: str
|
|
269
|
+
read: Callable
|
|
270
|
+
template: bool
|
|
271
|
+
|
|
272
|
+
|
|
205
273
|
class _ToolOptions(NamedTuple):
|
|
206
274
|
annotations: dict | None
|
|
207
275
|
structured_output: bool | None
|
|
@@ -239,9 +307,13 @@ class AgentEnvApplication(ABC):
|
|
|
239
307
|
self._app: Any = None
|
|
240
308
|
ext_handlers = _served_extensions(handler, _discover_extensions(handler))
|
|
241
309
|
environment_card = _merge_extensions(environment_card, ext_handlers + _declared_extensions(handler, ext_handlers))
|
|
242
|
-
tool_handlers = _served_tools(handler, _discover_tools(handler, environment_card.name))
|
|
310
|
+
tool_handlers = _served_tools(handler, _with_forwarded(_discover_tools(handler, environment_card.name), handler))
|
|
243
311
|
if tool_handlers and not self.supports_tools:
|
|
244
312
|
raise ValueError(f"@tool methods require a FastMCP-backed application; {type(self).__name__} cannot serve MCP tools")
|
|
313
|
+
self._resources = _discover_resources(handler)
|
|
314
|
+
if self._resources and not self.supports_tools:
|
|
315
|
+
raise ValueError(f"@resource methods require a FastMCP-backed application; {type(self).__name__} cannot "
|
|
316
|
+
"serve MCP resources")
|
|
245
317
|
environment_card = _merge_tools(environment_card, tool_handlers)
|
|
246
318
|
self._tool_access = (
|
|
247
319
|
ToolAccess(descriptor.name for descriptor, fn in tool_handlers if getattr(fn, _HIDDEN_BY_DEFAULT_ATTR, False))
|
|
@@ -276,6 +348,8 @@ class AgentEnvApplication(ABC):
|
|
|
276
348
|
self._add_route(app, path, [method], handler)
|
|
277
349
|
for descriptor, fn in self._tools:
|
|
278
350
|
self._register_tool(app, descriptor, fn)
|
|
351
|
+
for resource in self._resources:
|
|
352
|
+
self._register_resource(app, resource)
|
|
279
353
|
# Last, so it is the outermost layer: a dispatch guard the handler installed before mounting
|
|
280
354
|
# then runs under the bound ToolContext instead of outside it.
|
|
281
355
|
self._bind_tool_calls(app)
|
|
@@ -287,6 +361,9 @@ class AgentEnvApplication(ABC):
|
|
|
287
361
|
def _register_tool(self, app: Any, descriptor: EnvironmentTool, fn: Callable) -> None:
|
|
288
362
|
raise NotImplementedError(f"{type(self).__name__} does not serve MCP tools")
|
|
289
363
|
|
|
364
|
+
def _register_resource(self, app: Any, resource: _ServedResource) -> None:
|
|
365
|
+
raise NotImplementedError(f"{type(self).__name__} does not serve MCP resources")
|
|
366
|
+
|
|
290
367
|
def _check_tool_binding(self, app: Any) -> None:
|
|
291
368
|
"""Everything that can refuse the mount, before the app is touched; nothing to refuse without a tool dispatch."""
|
|
292
369
|
return None
|
|
@@ -329,6 +406,8 @@ class AgentEnvFastMCPApplication(AgentEnvApplication):
|
|
|
329
406
|
app.custom_route(path, methods=methods)(handler)
|
|
330
407
|
|
|
331
408
|
def _register_tool(self, app: Any, descriptor: EnvironmentTool, fn: Callable) -> None:
|
|
409
|
+
if (forwarded := getattr(fn, _FORWARDED_ATTR, None)) is not None:
|
|
410
|
+
return self._register_forwarded(app, descriptor, fn, forwarded)
|
|
332
411
|
# passed only when set, so a FastMCP-shaped target without the parameters still mounts plain tools
|
|
333
412
|
options = getattr(fn, _TOOL_OPTIONS_ATTR, None) or _ToolOptions(None, None, None)
|
|
334
413
|
fastmcp = self._binding == "middleware"
|
|
@@ -348,6 +427,38 @@ class AgentEnvFastMCPApplication(AgentEnvApplication):
|
|
|
348
427
|
extra["output_schema"] = None if options.structured_output is False else schema
|
|
349
428
|
app.tool(name=descriptor.name, description=descriptor.description, **extra)(registered)
|
|
350
429
|
|
|
430
|
+
def _register_forwarded(self, app: Any, descriptor: EnvironmentTool, fn: Callable, forwarded: ForwardedTool) -> None:
|
|
431
|
+
"""Put a forwarded tool into mcp's FastMCP tool manager as it is, with its own schema and its arguments passed
|
|
432
|
+
through untouched, since FastMCP's ``tool`` builds a schema from a signature."""
|
|
433
|
+
from mcp.server.fastmcp.tools.base import Tool
|
|
434
|
+
from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata
|
|
435
|
+
|
|
436
|
+
if self._binding != "dispatch":
|
|
437
|
+
raise RuntimeError(f"the forwarded tool {descriptor.name!r} needs mcp's FastMCP, whose tool manager takes a "
|
|
438
|
+
"tool with its own schema")
|
|
439
|
+
schema = forwarded.output_schema
|
|
440
|
+
app._tool_manager._tools[descriptor.name] = Tool(
|
|
441
|
+
fn=fn, name=descriptor.name, description=descriptor.description or "",
|
|
442
|
+
parameters=descriptor.inputSchema or {"type": "object"},
|
|
443
|
+
fn_metadata=FuncMetadata(arg_model=_passed_through(), output_schema=schema,
|
|
444
|
+
output_model=_as_it_came() if schema is not None else None),
|
|
445
|
+
is_async=True,
|
|
446
|
+
annotations=_mcp_tool_annotations(forwarded.annotations) if forwarded.annotations is not None else None)
|
|
447
|
+
|
|
448
|
+
def _register_resource(self, app: Any, resource: _ServedResource) -> None:
|
|
449
|
+
"""A resource as it is, its description absent when it has none, or a template through FastMCP's own
|
|
450
|
+
decorator."""
|
|
451
|
+
from mcp.server.fastmcp.resources import FunctionResource
|
|
452
|
+
|
|
453
|
+
if self._binding != "dispatch":
|
|
454
|
+
raise RuntimeError(f"the resource {resource.uri!r} needs mcp's FastMCP")
|
|
455
|
+
if resource.template:
|
|
456
|
+
app.resource(resource.uri, name=resource.name, description=resource.description,
|
|
457
|
+
mime_type=resource.mime_type)(resource.read)
|
|
458
|
+
return
|
|
459
|
+
app.add_resource(FunctionResource(uri=resource.uri, name=resource.name, description=resource.description,
|
|
460
|
+
mime_type=resource.mime_type, fn=resource.read))
|
|
461
|
+
|
|
351
462
|
def _check_tool_binding(self, app: Any) -> None:
|
|
352
463
|
"""Decide, before any route or tool is added, how every tools/call on ``app`` gets its ToolContext, and
|
|
353
464
|
refuse what cannot be honoured. The standalone ``fastmcp`` package (a ``middleware`` list with
|
|
@@ -480,7 +591,7 @@ def create_fastmcp_app(handler: Any, *, card: EnvironmentCard, **fastmcp_kwargs:
|
|
|
480
591
|
from mcp.server.fastmcp import FastMCP
|
|
481
592
|
except ImportError as e:
|
|
482
593
|
raise ImportError("create_fastmcp_app requires the 'mcp' package: pip install 'mcp>=1.25,<2'") from e
|
|
483
|
-
app = FastMCP(card.name, **fastmcp_kwargs)
|
|
594
|
+
app = FastMCP(card.name, **{"instructions": _instructions(handler), **fastmcp_kwargs})
|
|
484
595
|
app.settings.host = os.environ.get("MCP_HOST", "0.0.0.0")
|
|
485
596
|
app.settings.port = int(os.environ.get("MCP_PORT", "18765"))
|
|
486
597
|
app.settings.transport_security.enable_dns_rebinding_protection = False
|
|
@@ -564,6 +675,11 @@ class AgentEnvEnvironment:
|
|
|
564
675
|
"""The ``@tool`` methods to serve, chosen at mount; a returned descriptor's description replaces the marked one."""
|
|
565
676
|
return tools
|
|
566
677
|
|
|
678
|
+
def forwarded_tools(self) -> list[ForwardedTool]:
|
|
679
|
+
"""Tools served on another server's behalf, each with its own schema, beside the ``@tool`` methods: listed on
|
|
680
|
+
the card, chosen by ``served_tools`` and under tool access like them. Needs mcp's FastMCP."""
|
|
681
|
+
return []
|
|
682
|
+
|
|
567
683
|
def on_tool_call(self, context: ToolContext) -> Any:
|
|
568
684
|
"""Runs before every tools/call on this environment's app, under the bound ``context``.
|
|
569
685
|
|
|
@@ -932,6 +1048,47 @@ def _marked_members(handler: Any, marker_attr: str):
|
|
|
932
1048
|
yield attr, marker
|
|
933
1049
|
|
|
934
1050
|
|
|
1051
|
+
def _discover_resources(handler: Any) -> list[_ServedResource]:
|
|
1052
|
+
"""The resources and resource templates the handler's @resource methods serve; a URI served twice is refused."""
|
|
1053
|
+
served: list[_ServedResource] = []
|
|
1054
|
+
for method, marker in _marked_members(handler, _RESOURCE_ATTR):
|
|
1055
|
+
name, description = marker.name or method.__name__, marker.description or inspect.getdoc(method)
|
|
1056
|
+
if marker.listed is None:
|
|
1057
|
+
served.append(_ServedResource(marker.uri, name, description, marker.mime_type, method, "{" in marker.uri))
|
|
1058
|
+
continue
|
|
1059
|
+
for entry in marker.listed(handler):
|
|
1060
|
+
parts = _template_parts(marker.uri, entry["uri"])
|
|
1061
|
+
if parts is None:
|
|
1062
|
+
raise ValueError(f"the listed resource {entry['uri']!r} doesn't match {marker.uri!r}")
|
|
1063
|
+
served.append(_ServedResource(entry["uri"], entry["name"], entry.get("description"),
|
|
1064
|
+
entry.get("mimeType") or marker.mime_type, _with_parts(method, parts), False))
|
|
1065
|
+
uris = [resource.uri for resource in served]
|
|
1066
|
+
if duplicated := sorted({uri for uri in uris if uris.count(uri) > 1}):
|
|
1067
|
+
raise ValueError(f"resources served twice: {duplicated}")
|
|
1068
|
+
return served
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def _template_parts(template: str, uri: str) -> dict[str, str] | None:
|
|
1072
|
+
"""The parts of ``uri`` that ``template``'s ``{name}`` slots match, each within one path segment."""
|
|
1073
|
+
match = re.fullmatch(re.sub(r"\\\{(\w+)\\\}", r"(?P<\1>[^/]+)", re.escape(template)), uri)
|
|
1074
|
+
return match.groupdict() if match else None
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def _with_parts(method: Callable, parts: dict[str, str]) -> Callable:
|
|
1078
|
+
def read() -> Any:
|
|
1079
|
+
return method(**parts)
|
|
1080
|
+
|
|
1081
|
+
return read
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def _instructions(handler: Any) -> str | None:
|
|
1085
|
+
"""What the handler's @server_instructions method returns; None without one."""
|
|
1086
|
+
marked = [method for method, _marker in _marked_members(handler, _INSTRUCTIONS_ATTR)]
|
|
1087
|
+
if len(marked) > 1:
|
|
1088
|
+
raise ValueError(f"an environment has one @server_instructions method, not {sorted(m.__name__ for m in marked)}")
|
|
1089
|
+
return marked[0]() if marked else None
|
|
1090
|
+
|
|
1091
|
+
|
|
935
1092
|
def _discover_methods(handler: Any) -> dict:
|
|
936
1093
|
found: dict[str, Callable] = {}
|
|
937
1094
|
for attr, op in _marked_members(handler, _OP_ATTR):
|
|
@@ -1080,6 +1237,48 @@ def _served_operations(handler: Any, methods: dict) -> dict:
|
|
|
1080
1237
|
return {name: entry for name, entry in methods.items() if name in chosen}
|
|
1081
1238
|
|
|
1082
1239
|
|
|
1240
|
+
def _with_forwarded(tool_handlers: list, handler: Any) -> list:
|
|
1241
|
+
"""The ``@tool`` handlers and then the handler's forwarded tools, each as (descriptor, call), the call marked so it
|
|
1242
|
+
registers with its own schema; a forwarded tool named like another tool is refused."""
|
|
1243
|
+
hook = getattr(handler, "forwarded_tools", None)
|
|
1244
|
+
forwarded = [(entry.tool, _forwarding(entry)) for entry in (hook() if hook is not None else [])]
|
|
1245
|
+
names = [descriptor.name for descriptor, _fn in tool_handlers + forwarded]
|
|
1246
|
+
if len(set(names)) != len(names):
|
|
1247
|
+
raise ValueError(f"forwarded tools named like another tool: {sorted({n for n in names if names.count(n) > 1})}")
|
|
1248
|
+
return tool_handlers + forwarded
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def _forwarding(forwarded: ForwardedTool) -> Callable:
|
|
1252
|
+
async def call(**arguments: Any) -> Any:
|
|
1253
|
+
return await forwarded.call(**arguments)
|
|
1254
|
+
|
|
1255
|
+
setattr(call, _FORWARDED_ATTR, forwarded)
|
|
1256
|
+
return call
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
@functools.cache
|
|
1260
|
+
def _as_it_came() -> type:
|
|
1261
|
+
"""mcp's FastMCP output model for a forwarded tool with an output schema: the other server's result, unchecked."""
|
|
1262
|
+
from pydantic import RootModel
|
|
1263
|
+
|
|
1264
|
+
return RootModel[Any]
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
@functools.cache
|
|
1268
|
+
def _passed_through() -> type:
|
|
1269
|
+
"""mcp's FastMCP argument model for a forwarded tool: any arguments, each passed to the call as it came."""
|
|
1270
|
+
from mcp.server.fastmcp.utilities.func_metadata import ArgModelBase
|
|
1271
|
+
from pydantic import ConfigDict
|
|
1272
|
+
|
|
1273
|
+
class PassedThrough(ArgModelBase):
|
|
1274
|
+
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
|
|
1275
|
+
|
|
1276
|
+
def model_dump_one_level(self) -> dict[str, Any]:
|
|
1277
|
+
return dict(self.__pydantic_extra__ or {})
|
|
1278
|
+
|
|
1279
|
+
return PassedThrough
|
|
1280
|
+
|
|
1281
|
+
|
|
1083
1282
|
def _served_tools(handler: Any, tool_handlers: list) -> list:
|
|
1084
1283
|
"""The tools the handler's ``served_tools`` keeps, in its order and with its descriptors; all of them without one."""
|
|
1085
1284
|
hook = getattr(handler, "served_tools", None)
|
|
@@ -60,6 +60,7 @@ from .operations import (
|
|
|
60
60
|
type_text,
|
|
61
61
|
write_files,
|
|
62
62
|
)
|
|
63
|
+
from .keys import PRINTABLE_KEYS
|
|
63
64
|
from .manifest import ManifestSegment, RecordingManifest
|
|
64
65
|
from .platform import Platform
|
|
65
66
|
from .settings import SETTINGS_ENV, ComputerSettings
|
|
@@ -100,6 +101,7 @@ __all__ = [
|
|
|
100
101
|
"AppDeclaration",
|
|
101
102
|
"APP_SETTINGS_ENV",
|
|
102
103
|
"Display", "Image", "Effect", "ExecResult", "ReadyCheck", "Platform", "ComputerSettings", "SETTINGS_ENV",
|
|
104
|
+
"PRINTABLE_KEYS",
|
|
103
105
|
"display", "set_display", "capture", "click", "drag", "scroll", "type_text", "press",
|
|
104
106
|
"pointer_move", "pointer_down", "pointer_up", "pointer_position", "key_down", "key_up",
|
|
105
107
|
"run_in_session", "write_files", "record", "Recording", "RecordHints", "RecordedFrame", "RecordedSegment",
|
|
@@ -111,6 +111,11 @@ class ComputerDriverEnvironment(Tools, Extensions, DataPlane, AgentEnvEnvironmen
|
|
|
111
111
|
"""Checks of the driver's own, run with the base class's by computer-ready/v1; never sends input."""
|
|
112
112
|
return []
|
|
113
113
|
|
|
114
|
+
async def bundled_skill(self) -> tuple[str, str, dict[str, str]] | None:
|
|
115
|
+
"""The skill the software this driver drives ships for agents, as its description, its SKILL.md body without
|
|
116
|
+
frontmatter, and its other files by relative path; the computer skill leads with it. None by default."""
|
|
117
|
+
return None
|
|
118
|
+
|
|
114
119
|
def create_app(self) -> Any:
|
|
115
120
|
self._computer_require_init()
|
|
116
121
|
return super().create_app()
|
|
@@ -38,7 +38,7 @@ class Extensions:
|
|
|
38
38
|
mounted_card: Callable[[], EnvironmentCard]
|
|
39
39
|
|
|
40
40
|
@agent_skill
|
|
41
|
-
async def _computer_skill(self) -> tuple[str, str]:
|
|
41
|
+
async def _computer_skill(self) -> tuple[str, str, dict[str, str]]:
|
|
42
42
|
return await skill(self)
|
|
43
43
|
|
|
44
44
|
@extension(COMPUTER_READY, op="status", method="GET",
|
|
@@ -6,6 +6,8 @@ import difflib
|
|
|
6
6
|
|
|
7
7
|
from .errors import InvalidArgument, Unsupported
|
|
8
8
|
|
|
9
|
+
#: The printable ASCII characters but the space, each the key value it types.
|
|
10
|
+
PRINTABLE_KEYS = tuple(chr(code) for code in range(0x21, 0x7F))
|
|
9
11
|
NAMED = (
|
|
10
12
|
"Alt", "AltGraph", "CapsLock", "Control", "Fn", "FnLock", "Hyper", "Meta", "NumLock", "ScrollLock", "Shift",
|
|
11
13
|
"Super", "Symbol", "SymbolLock",
|