composeai 0.2.1__tar.gz → 0.3.0__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.
Files changed (94) hide show
  1. {composeai-0.2.1 → composeai-0.3.0}/PKG-INFO +7 -8
  2. {composeai-0.2.1 → composeai-0.3.0}/README.md +2 -7
  3. {composeai-0.2.1 → composeai-0.3.0}/docs/agents.md +2 -0
  4. {composeai-0.2.1 → composeai-0.3.0}/docs/index.md +1 -0
  5. composeai-0.3.0/docs/mcp.md +139 -0
  6. {composeai-0.2.1 → composeai-0.3.0}/pyproject.toml +4 -3
  7. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/__init__.py +3 -1
  8. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/errors.py +11 -0
  9. composeai-0.3.0/src/composeai/mcp.py +530 -0
  10. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/tools.py +28 -5
  11. composeai-0.3.0/tests/fixtures/mcp_fixture_server.py +49 -0
  12. {composeai-0.2.1 → composeai-0.3.0}/tests/test_cli.py +4 -3
  13. {composeai-0.2.1 → composeai-0.3.0}/tests/test_errors.py +8 -0
  14. {composeai-0.2.1 → composeai-0.3.0}/tests/test_live_smoke.py +19 -0
  15. composeai-0.3.0/tests/test_mcp.py +48 -0
  16. composeai-0.3.0/tests/test_mcp_e2e.py +279 -0
  17. {composeai-0.2.1 → composeai-0.3.0}/tests/test_package.py +1 -1
  18. {composeai-0.2.1 → composeai-0.3.0}/tests/test_tools.py +48 -0
  19. composeai-0.3.0/uv.lock +1332 -0
  20. composeai-0.2.1/uv.lock +0 -674
  21. {composeai-0.2.1 → composeai-0.3.0}/.github/workflows/release.yml +0 -0
  22. {composeai-0.2.1 → composeai-0.3.0}/.gitignore +0 -0
  23. {composeai-0.2.1 → composeai-0.3.0}/LICENSE +0 -0
  24. {composeai-0.2.1 → composeai-0.3.0}/compose.md +0 -0
  25. {composeai-0.2.1 → composeai-0.3.0}/docs/budgets.md +0 -0
  26. {composeai-0.2.1 → composeai-0.3.0}/docs/composition.md +0 -0
  27. {composeai-0.2.1 → composeai-0.3.0}/docs/flows.md +0 -0
  28. {composeai-0.2.1 → composeai-0.3.0}/docs/observability.md +0 -0
  29. {composeai-0.2.1 → composeai-0.3.0}/docs/providers.md +0 -0
  30. {composeai-0.2.1 → composeai-0.3.0}/docs/testing.md +0 -0
  31. {composeai-0.2.1 → composeai-0.3.0}/examples/quickstart_agent.py +0 -0
  32. {composeai-0.2.1 → composeai-0.3.0}/examples/research_flow.py +0 -0
  33. {composeai-0.2.1 → composeai-0.3.0}/examples/streaming_chat.py +0 -0
  34. {composeai-0.2.1 → composeai-0.3.0}/scripts/release.sh +0 -0
  35. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/_encoding.py +0 -0
  36. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/_ids.py +0 -0
  37. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/_schema.py +0 -0
  38. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/agentfn.py +0 -0
  39. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/cli.py +0 -0
  40. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/combinators.py +0 -0
  41. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/events.py +0 -0
  42. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/flow.py +0 -0
  43. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/hitl.py +0 -0
  44. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/messages.py +0 -0
  45. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/__init__.py +0 -0
  46. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/anthropic.py +0 -0
  47. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/base.py +0 -0
  48. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/compatible.py +0 -0
  49. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/openai.py +0 -0
  50. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/prices.py +0 -0
  51. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/registry.py +0 -0
  52. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/py.typed +0 -0
  53. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/runs.py +0 -0
  54. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/testing.py +0 -0
  55. {composeai-0.2.1 → composeai-0.3.0}/src/composeai/tracing.py +0 -0
  56. {composeai-0.2.1 → composeai-0.3.0}/tests/conformance/__init__.py +0 -0
  57. {composeai-0.2.1 → composeai-0.3.0}/tests/conformance/contract.py +0 -0
  58. {composeai-0.2.1 → composeai-0.3.0}/tests/conftest.py +0 -0
  59. {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_flow_defs.py +0 -0
  60. {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_hitl_agent_defs.py +0 -0
  61. {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_hitl_agent_run_a.py +0 -0
  62. {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_hitl_agent_run_b.py +0 -0
  63. {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_hitl_flow_defs.py +0 -0
  64. {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_hitl_run_a.py +0 -0
  65. {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_hitl_run_b.py +0 -0
  66. {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_run_a.py +0 -0
  67. {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_run_b.py +0 -0
  68. {composeai-0.2.1 → composeai-0.3.0}/tests/test_agent.py +0 -0
  69. {composeai-0.2.1 → composeai-0.3.0}/tests/test_agent_cache.py +0 -0
  70. {composeai-0.2.1 → composeai-0.3.0}/tests/test_anthropic.py +0 -0
  71. {composeai-0.2.1 → composeai-0.3.0}/tests/test_budget.py +0 -0
  72. {composeai-0.2.1 → composeai-0.3.0}/tests/test_cassettes.py +0 -0
  73. {composeai-0.2.1 → composeai-0.3.0}/tests/test_combinators.py +0 -0
  74. {composeai-0.2.1 → composeai-0.3.0}/tests/test_compatible.py +0 -0
  75. {composeai-0.2.1 → composeai-0.3.0}/tests/test_encoding.py +0 -0
  76. {composeai-0.2.1 → composeai-0.3.0}/tests/test_events.py +0 -0
  77. {composeai-0.2.1 → composeai-0.3.0}/tests/test_flow.py +0 -0
  78. {composeai-0.2.1 → composeai-0.3.0}/tests/test_hitl_agent.py +0 -0
  79. {composeai-0.2.1 → composeai-0.3.0}/tests/test_hitl_flow.py +0 -0
  80. {composeai-0.2.1 → composeai-0.3.0}/tests/test_hitl_subprocess.py +0 -0
  81. {composeai-0.2.1 → composeai-0.3.0}/tests/test_ids.py +0 -0
  82. {composeai-0.2.1 → composeai-0.3.0}/tests/test_messages.py +0 -0
  83. {composeai-0.2.1 → composeai-0.3.0}/tests/test_models_base.py +0 -0
  84. {composeai-0.2.1 → composeai-0.3.0}/tests/test_openai.py +0 -0
  85. {composeai-0.2.1 → composeai-0.3.0}/tests/test_prices.py +0 -0
  86. {composeai-0.2.1 → composeai-0.3.0}/tests/test_registry.py +0 -0
  87. {composeai-0.2.1 → composeai-0.3.0}/tests/test_render.py +0 -0
  88. {composeai-0.2.1 → composeai-0.3.0}/tests/test_resume_subprocess.py +0 -0
  89. {composeai-0.2.1 → composeai-0.3.0}/tests/test_runstore.py +0 -0
  90. {composeai-0.2.1 → composeai-0.3.0}/tests/test_schema_register.py +0 -0
  91. {composeai-0.2.1 → composeai-0.3.0}/tests/test_streaming.py +0 -0
  92. {composeai-0.2.1 → composeai-0.3.0}/tests/test_testing.py +0 -0
  93. {composeai-0.2.1 → composeai-0.3.0}/tests/test_tracing.py +0 -0
  94. {composeai-0.2.1 → composeai-0.3.0}/tests/test_tracing_sink.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: composeai
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Radically simple, functional framework for multi-agent AI workflows — typed agent functions, pipe composition, always-on local tracing, durable flows.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -8,16 +8,20 @@ Requires-Python: >=3.10
8
8
  Requires-Dist: pydantic>=2.7
9
9
  Provides-Extra: all
10
10
  Requires-Dist: anthropic>=0.40; extra == 'all'
11
+ Requires-Dist: mcp<2,>=1.27; extra == 'all'
11
12
  Requires-Dist: openai>=1.60; extra == 'all'
12
13
  Provides-Extra: anthropic
13
14
  Requires-Dist: anthropic>=0.40; extra == 'anthropic'
14
15
  Provides-Extra: dev
15
16
  Requires-Dist: anthropic>=0.40; extra == 'dev'
16
17
  Requires-Dist: build; extra == 'dev'
18
+ Requires-Dist: mcp<2,>=1.27; extra == 'dev'
17
19
  Requires-Dist: openai>=1.60; extra == 'dev'
18
20
  Requires-Dist: pyright>=1.1.380; extra == 'dev'
19
21
  Requires-Dist: pytest>=8; extra == 'dev'
20
22
  Requires-Dist: ruff>=0.5; extra == 'dev'
23
+ Provides-Extra: mcp
24
+ Requires-Dist: mcp<2,>=1.27; extra == 'mcp'
21
25
  Provides-Extra: openai
22
26
  Requires-Dist: openai>=1.60; extra == 'openai'
23
27
  Description-Content-Type: text/markdown
@@ -32,6 +36,7 @@ A radically simple, functional framework for multi-agent AI workflows.
32
36
  - **Flows are durable.** A `@flow` journals every step; if it crashes — or pauses on a **named interrupt** (`approve("publish")`) waiting for a human — `resume(run_id)` continues it in the same process or a brand-new one, days later, replaying finished steps without re-paying for them.
33
37
  - **Every run can carry a spend cap.** `Budget(usd=..., tokens=...)` is enforced after every LLM call in a run's subtree, and stays cumulative across `resume()` — a run can't dodge its budget by crashing and getting resumed.
34
38
  - **Streaming and tracing are the same event bus.** `.stream()` yields `text_delta`/`thinking_delta`/`tool_call_started`/`tool_args_delta` interleaved with the very `span_started`/`span_finished`/`run_finished` events the trace is built from — on agents, pipelines, and flows alike, so a live UI and the trace can never disagree.
39
+ - **MCP servers plug straight into `tools=`.** `compose.mcp_tools(command=..., ...)` connects to a Model Context Protocol server (stdio or streamable HTTP) and turns its tools into ordinary composeai `Tool` objects — indistinguishable from `@compose.tool` ones, including the same `requires_approval=` pause/resume.
35
40
 
36
41
  Runtime dependencies: **pydantic + the standard library**. Provider SDKs are optional extras. Python ≥ 3.10.
37
42
 
@@ -102,6 +107,7 @@ No accounts, no exporters, no instrumentation to wire up — the trace (and its
102
107
  | [docs/observability.md](docs/observability.md) | The local tracing model, every `compose` CLI command, `--import`, `COMPOSE_TRACE_CONTENT` |
103
108
  | [docs/budgets.md](docs/budgets.md) | `Budget(usd=, tokens=)`, what counts, cumulative spend across `resume()`, `BudgetExceededError` |
104
109
  | [docs/testing.md](docs/testing.md) | `FakeModel`, cassettes, `@agent(cache=True)`, `reset_registries()` |
110
+ | [docs/mcp.md](docs/mcp.md) | Connect MCP servers' tools to your agents |
105
111
 
106
112
  ## Rules of the road
107
113
 
@@ -127,13 +133,6 @@ The contracts composeai holds you to — and the ones it holds itself to:
127
133
  | **Durability & HITL** | Separate checkpointer/orchestrator machinery | Journaled `@flow` + named interrupts, one `resume()` |
128
134
  | **Dependencies** | Heavy transitive footprint | pydantic + stdlib; provider SDKs as optional extras |
129
135
 
130
- ## Releasing (maintainers)
131
-
132
- Two equivalent paths:
133
-
134
- - **Local**: put a project-scoped PyPI token in `.env` (git-ignored; see the placeholder), commit your work, then `scripts/release.sh X.Y.Z` — it bumps the version, runs the full gate, builds, and uploads. Commit the bump and tag `vX.Y.Z` afterwards.
135
- - **GitHub**: publish a GitHub Release and `.github/workflows/release.yml` tests, builds, and publishes via PyPI trusted publishing (no stored token). One-time setup: add this repo as a trusted publisher on pypi.org (workflow `release.yml`, environment `pypi`).
136
-
137
136
  ## Roadmap
138
137
 
139
138
  - OpenTelemetry exporter (the span model already tracks `gen_ai.*` attribute conventions)
@@ -8,6 +8,7 @@ A radically simple, functional framework for multi-agent AI workflows.
8
8
  - **Flows are durable.** A `@flow` journals every step; if it crashes — or pauses on a **named interrupt** (`approve("publish")`) waiting for a human — `resume(run_id)` continues it in the same process or a brand-new one, days later, replaying finished steps without re-paying for them.
9
9
  - **Every run can carry a spend cap.** `Budget(usd=..., tokens=...)` is enforced after every LLM call in a run's subtree, and stays cumulative across `resume()` — a run can't dodge its budget by crashing and getting resumed.
10
10
  - **Streaming and tracing are the same event bus.** `.stream()` yields `text_delta`/`thinking_delta`/`tool_call_started`/`tool_args_delta` interleaved with the very `span_started`/`span_finished`/`run_finished` events the trace is built from — on agents, pipelines, and flows alike, so a live UI and the trace can never disagree.
11
+ - **MCP servers plug straight into `tools=`.** `compose.mcp_tools(command=..., ...)` connects to a Model Context Protocol server (stdio or streamable HTTP) and turns its tools into ordinary composeai `Tool` objects — indistinguishable from `@compose.tool` ones, including the same `requires_approval=` pause/resume.
11
12
 
12
13
  Runtime dependencies: **pydantic + the standard library**. Provider SDKs are optional extras. Python ≥ 3.10.
13
14
 
@@ -78,6 +79,7 @@ No accounts, no exporters, no instrumentation to wire up — the trace (and its
78
79
  | [docs/observability.md](docs/observability.md) | The local tracing model, every `compose` CLI command, `--import`, `COMPOSE_TRACE_CONTENT` |
79
80
  | [docs/budgets.md](docs/budgets.md) | `Budget(usd=, tokens=)`, what counts, cumulative spend across `resume()`, `BudgetExceededError` |
80
81
  | [docs/testing.md](docs/testing.md) | `FakeModel`, cassettes, `@agent(cache=True)`, `reset_registries()` |
82
+ | [docs/mcp.md](docs/mcp.md) | Connect MCP servers' tools to your agents |
81
83
 
82
84
  ## Rules of the road
83
85
 
@@ -103,13 +105,6 @@ The contracts composeai holds you to — and the ones it holds itself to:
103
105
  | **Durability & HITL** | Separate checkpointer/orchestrator machinery | Journaled `@flow` + named interrupts, one `resume()` |
104
106
  | **Dependencies** | Heavy transitive footprint | pydantic + stdlib; provider SDKs as optional extras |
105
107
 
106
- ## Releasing (maintainers)
107
-
108
- Two equivalent paths:
109
-
110
- - **Local**: put a project-scoped PyPI token in `.env` (git-ignored; see the placeholder), commit your work, then `scripts/release.sh X.Y.Z` — it bumps the version, runs the full gate, builds, and uploads. Commit the bump and tag `vX.Y.Z` afterwards.
111
- - **GitHub**: publish a GitHub Release and `.github/workflows/release.yml` tests, builds, and publishes via PyPI trusted publishing (no stored token). One-time setup: add this repo as a trusted publisher on pypi.org (workflow `release.yml`, environment `pypi`).
112
-
113
108
  ## Roadmap
114
109
 
115
110
  - OpenTelemetry exporter (the span model already tracks `gen_ai.*` attribute conventions)
@@ -85,6 +85,8 @@ def fetch_url(url: str) -> str:
85
85
 
86
86
  When the model requests several tool calls in one turn, they run in parallel with no blanket bound on the batch as a whole — `@tool(timeout=...)` is the only per-call guard, so an individual tool with no timeout set can run indefinitely alongside its siblings.
87
87
 
88
+ Tools can also come from MCP servers — see [mcp](mcp.md).
89
+
88
90
  ## Resilience knobs
89
91
 
90
92
  All of these are keyword-only arguments to `@compose.agent(...)`:
@@ -74,6 +74,7 @@ No accounts, no exporters, no instrumentation to wire up — the trace (and its
74
74
  | [observability](observability.md) | The local tracing model, every `compose` CLI command, `--import`, `COMPOSE_TRACE_CONTENT` |
75
75
  | [budgets](budgets.md) | `Budget(usd=, tokens=)`, what counts, cumulative spend across `resume()`, `BudgetExceededError` |
76
76
  | [testing](testing.md) | `FakeModel`, cassettes, `@agent(cache=True)`, `reset_registries()` |
77
+ | [mcp](mcp.md) | Connect MCP servers' tools to your agents |
77
78
 
78
79
  ## See also
79
80
 
@@ -0,0 +1,139 @@
1
+ # MCP
2
+
3
+ `compose.mcp_tools(...)` connects to one [Model Context Protocol](https://modelcontextprotocol.io) server and turns every tool it offers into ordinary composeai `Tool` objects — pass the result straight into `@agent(tools=...)` alongside (or instead of) `@compose.tool`-decorated functions.
4
+
5
+ ## Connecting
6
+
7
+ Two transports, mutually exclusive: `command=` (stdio — spawns a subprocess) or `url=` (streamable HTTP). A filesystem server over stdio:
8
+
9
+ ```python
10
+ import composeai as compose
11
+
12
+ notes_tools = compose.mcp_tools(
13
+ command="npx",
14
+ args=["-y", "@modelcontextprotocol/server-filesystem", "/path/to/notes"],
15
+ )
16
+
17
+
18
+ @compose.agent(model="anthropic/claude-sonnet-5", tools=notes_tools)
19
+ def notes_assistant(question: str) -> str:
20
+ """You help answer questions about the files in the notes directory."""
21
+ return compose.prompt(question)
22
+ ```
23
+
24
+ `mcp_tools()` is eager: it connects, calls `initialize()`, and lists every tool the server offers (following pagination) before returning — there's no lazy handle. Called at module level, as above, that's an import-time connect, the same trade-off the module-level `@agent` idiom already makes for model clients.
25
+
26
+ For streamable HTTP, `url=` replaces `command=`/`args=`/`env=`, and `headers=` carries static headers such as an auth token:
27
+
28
+ ```python
29
+ weather_tools = compose.mcp_tools(
30
+ url="https://mcp.example.com/mcp",
31
+ headers={"Authorization": "Bearer sk-..."},
32
+ )
33
+ ```
34
+
35
+ Mixing transport-specific kwargs across transports is a `ConfigError`: `headers=` with `command=`, or `env=`/`args=` with `url=` — as is omitting or supplying both `command=` and `url=`.
36
+
37
+ ## Using the tools
38
+
39
+ An MCP tool is indistinguishable from a local `@compose.tool` once built — same `Tool` type, same place in the `tools=` list, same loop:
40
+
41
+ ```python
42
+ @compose.tool
43
+ def count_words(text: str) -> int:
44
+ """Count the words in a piece of text.
45
+
46
+ Args:
47
+ text: The text whose words should be counted.
48
+ """
49
+ return len(text.split())
50
+
51
+
52
+ @compose.agent(model="anthropic/claude-sonnet-5", tools=[count_words, *notes_tools])
53
+ def researcher(question: str) -> str:
54
+ """Answer questions, using local and MCP tools as needed."""
55
+ return compose.prompt(question)
56
+ ```
57
+
58
+ Each tool's description and JSON Schema come straight from the server's own `list_tools()` response (a missing description is replaced with a placeholder noting the server didn't provide one); the schema is never sealed to strict mode the way `@compose.tool`'s is, since a server's schema is outside composeai's control.
59
+
60
+ ## Selecting and naming
61
+
62
+ `include=`/`exclude=` filter by the server's original tool names, evaluated before `prefix=` is applied:
63
+
64
+ ```python
65
+ tools = compose.mcp_tools(
66
+ command="npx",
67
+ args=["-y", "@modelcontextprotocol/server-filesystem", "/path/to/notes"],
68
+ include=["read_file", "list_directory"],
69
+ prefix="fs",
70
+ )
71
+ ```
72
+
73
+ An unknown name in `include=`/`exclude=` (or in `requires_approval=` when given as a list, below) raises `ConfigError` naming the server's actual tool names, so a typo fails at construction instead of silently matching nothing. A server that returns two tools with the same name is also a `ConfigError` — fail loudly rather than silently shadow one.
74
+
75
+ `prefix=` renames each selected tool's spec to `f"{prefix}_{name}"` for the model — above, `read_file` becomes `fs_read_file` — but execution still calls the server with the tool's original name; the prefix exists purely to avoid name collisions when combining multiple servers' tools in one `tools=[...]` list:
76
+
77
+ ```python
78
+ fs_tools = compose.mcp_tools(command="npx", args=[...], prefix="fs")
79
+ db_tools = compose.mcp_tools(command="npx", args=[...], prefix="db")
80
+
81
+ @compose.agent(model="anthropic/claude-sonnet-5", tools=[*fs_tools, *db_tools])
82
+ def assistant(question: str) -> str: ...
83
+ ```
84
+
85
+ ## Approval
86
+
87
+ `requires_approval=` is `True` (every tool from this server pauses for approval), `False` (the default), or a list of the server's original tool names that should. It flows straight into `ToolSpec.requires_approval` — the same human-in-the-loop pause/resume machinery `@compose.tool(requires_approval=True)` uses, with no MCP-specific approval mechanism; see [flows](flows.md)'s human-in-the-loop section for the full story.
88
+
89
+ ```python
90
+ tools = compose.mcp_tools(
91
+ command="npx",
92
+ args=["-y", "@modelcontextprotocol/server-filesystem", "/path/to/notes"],
93
+ requires_approval=["write_file"],
94
+ )
95
+
96
+
97
+ @compose.agent(model="anthropic/claude-sonnet-5", tools=tools, name="notes_editor")
98
+ def notes_editor(instruction: str) -> str:
99
+ """Edit notes as instructed."""
100
+ return compose.prompt(instruction)
101
+
102
+
103
+ run = notes_editor.run("Update the meeting notes with today's action items")
104
+ # run.status == "paused" if the model called write_file
105
+ resumed = compose.resume(run.id, answers={"write_file": True})
106
+ ```
107
+
108
+ Same shorthand as a local tool's approval gate: the bare tool name (`"write_file"`) resolves to the reserved interrupt id `tool:write_file:{call_id}` when exactly one pending interrupt matches. `answers={"write_file": False}` denies the call instead — the model sees `"denied by user"` and carries on.
109
+
110
+ ## Timeouts
111
+
112
+ `timeout=` bounds each individual tool call (`None`, the default, falls back to `connect_timeout=`); `connect_timeout=` (default `30.0` seconds) bounds each phase of setup separately, not their sum — one full `connect_timeout=` budget for spawning the subprocess or opening the HTTP connection plus `initialize()`, and a second, separately-timed `connect_timeout=` budget for the initial `list_tools()` pagination. A server that's slow in both phases can take up to roughly `2 × connect_timeout` end-to-end:
113
+
114
+ ```python
115
+ tools = compose.mcp_tools(
116
+ command="npx",
117
+ args=["-y", "@modelcontextprotocol/server-filesystem", "/path/to/notes"],
118
+ timeout=10.0,
119
+ connect_timeout=15.0,
120
+ )
121
+ ```
122
+
123
+ A tool call that exceeds `timeout=` (or otherwise fails — the server reported `isError`, the transport broke mid-call) raises `MCPToolError` internally, which surfaces to the model exactly like any tool-body exception: an `is_error` tool result the model can react to, never a run abort. The agent keeps running.
124
+
125
+ ## Lifecycle & caveats
126
+
127
+ - **Process-lifetime connections.** Each `mcp_tools()` call opens one connection that lives for the process, on a dedicated background thread — there's no per-call reconnect. Nothing closes it for you mid-run.
128
+ - **`composeai.mcp.close_all()`** closes every currently-live MCP server connection; idempotent and never raises. `mcp_tools()` registers it with `atexit` on its first successful call, so stdio subprocesses don't outlive the interpreter, but call it directly too (e.g. between test cases, or to release servers early).
129
+ - **Import-time connect.** A module-level `mcp_tools()` call (the natural home for the module-level `@agent` idiom) connects at import time, the same trade-off model-string resolution already makes — importing the module requires the server to be reachable.
130
+ - **A resumed flow needs the server reachable again.** The connection doesn't survive across processes: a paused run resumed in a fresh process re-executes the module top level, so `mcp_tools()` reconnects from scratch — if the server isn't reachable at resume time, that reconnect fails.
131
+ - **The tool list is a snapshot.** `mcp_tools()` lists tools once, at call time. If the server adds, removes, or changes tools afterward, the `Tool` objects already built don't see it — call `mcp_tools()` again to pick up changes.
132
+
133
+ ## Install
134
+
135
+ `pip install 'composeai[mcp]'` (pinned `mcp>=1.27,<2`, since SDK v2 is a from-scratch breaking redesign still to land).
136
+
137
+ ## See also
138
+
139
+ [agents](agents.md) covers the `tools=` list and the `@compose.tool` decorator MCP tools sit alongside; [flows](flows.md) covers `resume()` and the human-in-the-loop pause/resume mechanism `requires_approval=` reuses; [testing](testing.md) covers `FakeModel` for testing an agent's tool-calling behavior with no network, MCP or otherwise.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "composeai"
7
- version = "0.2.1"
7
+ version = "0.3.0"
8
8
  description = "Radically simple, functional framework for multi-agent AI workflows — typed agent functions, pipe composition, always-on local tracing, durable flows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -17,8 +17,9 @@ compose = "composeai.cli:main"
17
17
  [project.optional-dependencies]
18
18
  anthropic = ["anthropic>=0.40"]
19
19
  openai = ["openai>=1.60"]
20
- all = ["composeai[anthropic,openai]"]
21
- dev = ["pytest>=8", "ruff>=0.5", "pyright>=1.1.380", "anthropic>=0.40", "openai>=1.60", "build"]
20
+ mcp = ["mcp>=1.27,<2"]
21
+ all = ["composeai[anthropic,openai,mcp]"]
22
+ dev = ["pytest>=8", "ruff>=0.5", "pyright>=1.1.380", "anthropic>=0.40", "openai>=1.60", "mcp>=1.27,<2", "build"]
22
23
 
23
24
  [tool.hatch.build.targets.wheel]
24
25
  packages = ["src/composeai"]
@@ -10,12 +10,13 @@ from .agentfn import agent, prompt
10
10
  from .combinators import MapResult, aggregate, map, pipe
11
11
  from .flow import flow, now, random, resume, task
12
12
  from .hitl import Interrupt, approve, ask_human
13
+ from .mcp import mcp_tools
13
14
  from .models.compatible import openai_compatible
14
15
  from .models.prices import ModelPrice, register_price
15
16
  from .runs import Budget, Run
16
17
  from .tools import tool
17
18
 
18
- __version__ = "0.2.1"
19
+ __version__ = "0.3.0"
19
20
 
20
21
  __all__ = [
21
22
  "Budget",
@@ -29,6 +30,7 @@ __all__ = [
29
30
  "ask_human",
30
31
  "flow",
31
32
  "map",
33
+ "mcp_tools",
32
34
  "now",
33
35
  "openai_compatible",
34
36
  "pipe",
@@ -94,3 +94,14 @@ class TaskTimeoutError(ComposeError):
94
94
  daemon thread after this is raised; its eventual result or exception
95
95
  is discarded. Treat a timed-out task as failed and move on.
96
96
  """
97
+
98
+
99
+ class MCPToolError(ComposeError):
100
+ """An MCP tool call failed: the server reported ``isError``, the
101
+ transport failed mid-run, the per-call timeout expired, or the server
102
+ connection was already closed.
103
+
104
+ Inside an agent run this surfaces exactly like any tool-body
105
+ exception: an ``is_error`` tool result the model can react to -- never
106
+ a run abort (see ``composeai.agentfn._execute_one_tool``).
107
+ """