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.
- {composeai-0.2.1 → composeai-0.3.0}/PKG-INFO +7 -8
- {composeai-0.2.1 → composeai-0.3.0}/README.md +2 -7
- {composeai-0.2.1 → composeai-0.3.0}/docs/agents.md +2 -0
- {composeai-0.2.1 → composeai-0.3.0}/docs/index.md +1 -0
- composeai-0.3.0/docs/mcp.md +139 -0
- {composeai-0.2.1 → composeai-0.3.0}/pyproject.toml +4 -3
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/__init__.py +3 -1
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/errors.py +11 -0
- composeai-0.3.0/src/composeai/mcp.py +530 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/tools.py +28 -5
- composeai-0.3.0/tests/fixtures/mcp_fixture_server.py +49 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_cli.py +4 -3
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_errors.py +8 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_live_smoke.py +19 -0
- composeai-0.3.0/tests/test_mcp.py +48 -0
- composeai-0.3.0/tests/test_mcp_e2e.py +279 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_package.py +1 -1
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_tools.py +48 -0
- composeai-0.3.0/uv.lock +1332 -0
- composeai-0.2.1/uv.lock +0 -674
- {composeai-0.2.1 → composeai-0.3.0}/.github/workflows/release.yml +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/.gitignore +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/LICENSE +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/compose.md +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/docs/budgets.md +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/docs/composition.md +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/docs/flows.md +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/docs/observability.md +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/docs/providers.md +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/docs/testing.md +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/examples/quickstart_agent.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/examples/research_flow.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/examples/streaming_chat.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/scripts/release.sh +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/_encoding.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/_ids.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/_schema.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/agentfn.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/cli.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/combinators.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/events.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/flow.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/hitl.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/messages.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/__init__.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/anthropic.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/base.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/compatible.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/openai.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/prices.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/models/registry.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/py.typed +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/runs.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/testing.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/src/composeai/tracing.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/conformance/__init__.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/conformance/contract.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/conftest.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_flow_defs.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_hitl_agent_defs.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_hitl_agent_run_a.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_hitl_agent_run_b.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_hitl_flow_defs.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_hitl_run_a.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_hitl_run_b.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_run_a.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/fixtures/subprocess_run_b.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_agent.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_agent_cache.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_anthropic.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_budget.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_cassettes.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_combinators.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_compatible.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_encoding.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_events.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_flow.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_hitl_agent.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_hitl_flow.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_hitl_subprocess.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_ids.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_messages.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_models_base.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_openai.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_prices.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_registry.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_render.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_resume_subprocess.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_runstore.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_schema_register.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_streaming.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_testing.py +0 -0
- {composeai-0.2.1 → composeai-0.3.0}/tests/test_tracing.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
21
|
-
|
|
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.
|
|
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
|
+
"""
|