chuk-tool-processor 0.9.7__tar.gz → 0.11__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 (76) hide show
  1. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/PKG-INFO +806 -160
  2. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/README.md +805 -159
  3. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/pyproject.toml +37 -13
  4. chuk_tool_processor-0.11/src/chuk_tool_processor/__init__.py +117 -0
  5. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/core/exceptions.py +55 -4
  6. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/core/processor.py +365 -46
  7. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/__init__.py +5 -8
  8. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/context.py +2 -5
  9. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/__init__.py +4 -0
  10. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/mcp_tool.py +8 -3
  11. chuk_tool_processor-0.11/src/chuk_tool_processor/mcp/models.py +151 -0
  12. chuk_tool_processor-0.11/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +201 -0
  13. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/stream_manager.py +94 -0
  14. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_export_mixin.py +4 -4
  15. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/metrics.py +3 -3
  16. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/tracing.py +13 -12
  17. chuk_tool_processor-0.11/src/chuk_tool_processor/registry/__init__.py +110 -0
  18. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/decorators.py +42 -0
  19. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/interface.py +15 -10
  20. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/metadata.py +26 -0
  21. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/providers/__init__.py +2 -1
  22. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/providers/memory.py +23 -7
  23. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/tool_export.py +20 -23
  24. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/PKG-INFO +806 -160
  25. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/SOURCES.txt +2 -0
  26. chuk_tool_processor-0.9.7/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +0 -82
  27. chuk_tool_processor-0.9.7/src/chuk_tool_processor/registry/__init__.py +0 -60
  28. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/setup.cfg +0 -0
  29. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/core/__init__.py +0 -0
  30. {chuk_tool_processor-0.9.7/src/chuk_tool_processor → chuk_tool_processor-0.11/src/chuk_tool_processor/execution}/__init__.py +0 -0
  31. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
  32. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +0 -0
  33. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +0 -0
  34. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
  35. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
  36. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
  37. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/circuit_breaker.py +0 -0
  38. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
  39. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
  40. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/formatter.py +0 -0
  41. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/helpers.py +0 -0
  42. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/metrics.py +0 -0
  43. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
  44. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
  45. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
  46. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
  47. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
  48. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +0 -0
  49. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/models.py +0 -0
  50. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/sse_transport.py +0 -0
  51. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -0
  52. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/__init__.py +0 -0
  53. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
  54. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
  55. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_call.py +0 -0
  56. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_result.py +0 -0
  57. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_spec.py +0 -0
  58. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/validated_tool.py +0 -0
  59. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/__init__.py +0 -0
  60. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/setup.py +0 -0
  61. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/__init__.py +0 -0
  62. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/discovery.py +0 -0
  63. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
  64. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
  65. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
  66. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
  67. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
  68. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
  69. /chuk_tool_processor-0.9.7/src/chuk_tool_processor/execution/__init__.py → /chuk_tool_processor-0.11/src/chuk_tool_processor/py.typed +0 -0
  70. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/auto_register.py +0 -0
  71. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/provider.py +0 -0
  72. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/utils/__init__.py +0 -0
  73. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/utils/validation.py +0 -0
  74. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
  75. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/requires.txt +0 -0
  76. {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.9.7
3
+ Version: 0.11
4
4
  Summary: Async-native framework for registering, discovering, and executing tools referenced in LLM responses
5
5
  Author-email: CHUK Team <chrishayuk@somejunkmailbox.com>
6
6
  Maintainer-email: CHUK Team <chrishayuk@somejunkmailbox.com>
@@ -26,65 +26,147 @@ Requires-Dist: psutil>=7.0.0
26
26
  Requires-Dist: pydantic>=2.11.3
27
27
  Requires-Dist: uuid>=1.30
28
28
 
29
- # CHUK Tool Processor
29
+ # CHUK Tool Processor — Production-grade execution for LLM tool calls
30
30
 
31
31
  [![PyPI](https://img.shields.io/pypi/v/chuk-tool-processor.svg)](https://pypi.org/project/chuk-tool-processor/)
32
32
  [![Python](https://img.shields.io/pypi/pyversions/chuk-tool-processor.svg)](https://pypi.org/project/chuk-tool-processor/)
33
33
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
34
+ [![Type Checked](https://img.shields.io/badge/type%20checked-PEP%20561-blue.svg)](https://www.python.org/dev/peps/pep-0561/)
35
+ [![Wheels](https://img.shields.io/badge/wheels-macOS%20%7C%20Linux%20%7C%20Windows-blue.svg)](https://pypi.org/project/chuk-tool-processor/)
36
+ [![OpenTelemetry](https://img.shields.io/badge/observability-OpenTelemetry%20%7C%20Prometheus-blue.svg)](docs/OBSERVABILITY.md)
34
37
 
35
- **The missing link between LLM tool calls and reliable execution.**
38
+ **Reliable tool execution for LLMs timeouts, retries, caching, rate limits, circuit breakers, and MCP integration — in one composable layer.**
36
39
 
37
- CHUK Tool Processor is a focused, production-ready framework that solves one problem exceptionally well: **processing tool calls from LLM outputs**. It's not a chatbot framework or LLM orchestration platform—it's the glue layer that bridges LLM responses and actual tool execution.
40
+ ---
38
41
 
39
- ## The Problem
42
+ ## The Missing Layer for Reliable Tool Execution
40
43
 
41
- When you build LLM applications, you face a gap:
44
+ LLMs are good at *calling* tools. The hard part is **executing** those tools reliably.
42
45
 
43
- 1. **LLM generates tool calls** in various formats (XML tags, OpenAI `tool_calls`, JSON)
44
- 2. **??? Mystery step ???** where you need to:
45
- - Parse those calls reliably
46
- - Handle timeouts, retries, failures
47
- - Cache expensive results
48
- - Rate limit API calls
49
- - Run untrusted code safely
50
- - Connect to external tool servers
51
- - Log everything for debugging
52
- 3. **Get results back** to continue the LLM conversation
46
+ **CHUK Tool Processor:**
47
+ - Parses tool calls from any model (Anthropic XML, OpenAI `tool_calls`, JSON)
48
+ - Executes them with **timeouts, retries, caching, rate limits, circuit breaker, observability**
49
+ - Runs tools locally, in **isolated subprocesses**, or **remote via MCP**
53
50
 
54
- Most frameworks give you steps 1 and 3, but step 2 is where the complexity lives. CHUK Tool Processor **is** step 2.
51
+ CHUK Tool Processor is the execution layer between LLM responses and real tools.
55
52
 
56
- ## Why chuk-tool-processor?
53
+ It sits **below** agent frameworks and prompt orchestration, and **above** raw tool implementations.
57
54
 
58
- ### It's a Building Block, Not a Framework
55
+ ```
56
+ LLM Output
57
+
58
+ CHUK Tool Processor
59
+
60
+ ┌──────────────┬────────────────────┐
61
+ │ Local Tools │ Remote Tools (MCP) │
62
+ └──────────────┴────────────────────┘
63
+ ```
59
64
 
60
- Unlike full-fledged LLM frameworks (LangChain, LlamaIndex, etc.), CHUK Tool Processor:
65
+ **How it works internally:**
61
66
 
62
- - ✅ **Does one thing well**: Process tool calls reliably
63
- - ✅ **Plugs into any LLM app**: Works with any framework or no framework
64
- - ✅ **Composable by design**: Stack strategies and wrappers like middleware
65
- - **No opinions about your LLM**: Bring your own OpenAI, Anthropic, local model
66
- - ❌ **Doesn't manage conversations**: That's your job
67
- - ❌ **Doesn't do prompt engineering**: Use whatever prompting you want
68
- - ❌ **Doesn't bundle an LLM client**: Use any client library you prefer
67
+ ```
68
+ LLM Output
69
+
70
+ Parsers (XML / OpenAI / JSON)
71
+
72
+ ┌─────────────────────────────┐
73
+ │ Execution Middleware │
74
+ │ (Applied in this order) │
75
+ │ • Cache │
76
+ │ • Rate Limit │
77
+ │ • Retry (with backoff) │
78
+ │ • Circuit Breaker │
79
+ └─────────────────────────────┘
80
+
81
+ Execution Strategy
82
+ ┌──────────────────────┐
83
+ │ • InProcess │ ← Fast, trusted
84
+ │ • Isolated/Subprocess│ ← Safe, untrusted
85
+ │ • Remote via MCP │ ← Distributed
86
+ └──────────────────────┘
87
+ ```
69
88
 
70
- ### It's Built for Production
89
+ Works with OpenAI, Anthropic, local models (Ollama/MLX/vLLM), and any framework (LangChain, LlamaIndex, custom).
71
90
 
72
- Research code vs production code is about handling the edges:
91
+ ## Executive TL;DR
73
92
 
74
- - **Timeouts**: Every tool execution has proper timeout handling
75
- - **Retries**: Automatic retry with exponential backoff and deadline awareness
76
- - **Rate Limiting**: Global and per-tool rate limits with sliding windows
77
- - **Caching**: Intelligent result caching with TTL and idempotency key support
78
- - **Circuit Breakers**: Prevent cascading failures with automatic fault detection
79
- - **Error Handling**: Machine-readable error codes with structured details
80
- - **Observability**: Structured logging, metrics, request tracing
81
- - **Safety**: Subprocess isolation for untrusted code
82
- - **Type Safety**: Pydantic validation with LLM-friendly argument coercion
83
- - **Tool Discovery**: Formal schema export (OpenAI, Anthropic, MCP formats)
93
+ * **Parse any format:** `XML` (Anthropic), `OpenAI tool_calls`, or raw `JSON`
94
+ * **Execute with production policies:** timeouts/retries/cache/rate-limits/circuit-breaker/idempotency
95
+ * **Run anywhere:** locally (fast), isolated (subprocess sandbox), or remote via MCP (HTTP/STDIO/SSE)
84
96
 
85
- ### It's About Stacks
97
+ ```python
98
+ import asyncio
99
+ from chuk_tool_processor import ToolProcessor, tool
86
100
 
87
- CHUK Tool Processor uses a **composable stack architecture**:
101
+ @tool(name="weather") # Clean decorator syntax
102
+ class WeatherTool:
103
+ async def execute(self, city: str) -> dict:
104
+ return {"temp": 72, "condition": "sunny", "city": city}
105
+
106
+ async def main():
107
+ # No need for initialize() - auto-initializes on first use!
108
+ async with ToolProcessor(enable_caching=True, enable_retries=True) as p:
109
+ # Works with OpenAI, Anthropic, or JSON formats
110
+ result = await p.process('<tool name="weather" args=\'{"city": "SF"}\'/>')
111
+ print(result[0].result) # {'temp': 72, 'condition': 'sunny', 'city': 'SF'}
112
+
113
+ asyncio.run(main())
114
+ ```
115
+
116
+ > **If you only remember three things:**
117
+ >
118
+ > 1. **Parse** `XML`, `OpenAI tool_calls`, or raw `JSON` automatically
119
+ > 2. **Execute** with timeouts/retries/cache/rate-limits/circuit-breaker
120
+ > 3. **Run** tools locally, isolated (subprocess), or remote via MCP
121
+
122
+ ## When to Use This
123
+
124
+ Use **CHUK Tool Processor** when:
125
+ - Your LLM calls tools or APIs
126
+ - You need **retries, timeouts, caching, or rate limits**
127
+ - You need to **run untrusted tools safely**
128
+ - Your tools are **local or remote (MCP)**
129
+
130
+ Do **not** use this if:
131
+ - You want an agent framework
132
+ - You want conversation flow/memory orchestration
133
+
134
+ **This is the execution layer, not the agent.**
135
+
136
+ > **Not a framework.**
137
+ > If LangChain/LlamaIndex help decide *which* tool to call,
138
+ > CHUK Tool Processor makes sure the tool call **actually succeeds**.
139
+
140
+ ## Table of Contents
141
+
142
+ - [The Problem](#the-problem)
143
+ - [Why chuk-tool-processor?](#why-chuk-tool-processor)
144
+ - [Compatibility Matrix](#compatibility-matrix)
145
+ - [Developer Experience Highlights](#developer-experience-highlights)
146
+ - [Quick Start](#quick-start)
147
+ - [Documentation Quick Reference](#documentation-quick-reference)
148
+ - [Choose Your Path](#choose-your-path)
149
+ - [Core Concepts](#core-concepts)
150
+ - [Getting Started](#getting-started)
151
+ - [Advanced Topics](#advanced-topics)
152
+ - [Configuration](#configuration)
153
+ - [Architecture Principles](#architecture-principles)
154
+ - [Examples](#examples)
155
+ - [FAQ](#faq)
156
+ - [Comparison with Other Tools](#comparison-with-other-tools)
157
+ - [Development & Publishing](#development--publishing)
158
+ - [Stability & Versioning](#stability--versioning)
159
+ - [Contributing & Support](#contributing--support)
160
+
161
+ ## The Problem
162
+
163
+ LLMs generate tool calls. **The hard part is executing them reliably.**
164
+
165
+ CHUK Tool Processor **is that execution layer.**
166
+
167
+ ## Why chuk-tool-processor?
168
+
169
+ **Composable execution layers:**
88
170
 
89
171
  ```
90
172
  ┌─────────────────────────────────┐
@@ -104,7 +186,7 @@ CHUK Tool Processor uses a **composable stack architecture**:
104
186
  ├─────────────────────────────────┤
105
187
  │ Execution Strategy │ ← How to run tools
106
188
  │ • InProcess (fast) │
107
- │ • Subprocess (isolated) │
189
+ │ • Isolated (subprocess) │
108
190
  ├─────────────────────────────────┤
109
191
  │ Tool Registry │ ← Your registered tools
110
192
  └─────────────────────────────────┘
@@ -112,8 +194,40 @@ CHUK Tool Processor uses a **composable stack architecture**:
112
194
 
113
195
  Each layer is **optional** and **configurable**. Mix and match what you need.
114
196
 
197
+ ### It's a Building Block, Not a Framework
198
+
199
+ Unlike full-fledged LLM frameworks (LangChain, LlamaIndex, etc.), CHUK Tool Processor:
200
+
201
+ - ✅ **Does one thing well**: Process tool calls reliably
202
+ - ✅ **Plugs into any LLM app**: Works with any framework or no framework
203
+ - ✅ **Composable by design**: Stack strategies and wrappers like middleware
204
+ - ✅ **No opinions about your LLM**: Bring your own OpenAI, Anthropic, local model
205
+ - ❌ **Doesn't manage conversations**: That's your job
206
+ - ❌ **Doesn't do prompt engineering**: Use whatever prompting you want
207
+ - ❌ **Doesn't bundle an LLM client**: Use any client library you prefer
208
+
209
+ ### It's Built for Production
210
+
211
+ Research code vs production code is about handling the edges. CHUK Tool Processor includes:
212
+
213
+ - ✅ **Timeouts** — Every tool execution has proper timeout handling
214
+ - ✅ **Retries** — Automatic retry with exponential backoff and deadline awareness
215
+ - ✅ **Rate Limiting** — Global and per-tool rate limits with sliding windows → [CONFIGURATION.md](docs/CONFIGURATION.md)
216
+ - ✅ **Caching** — Intelligent result caching with TTL and idempotency key support
217
+ - ✅ **Circuit Breakers** — Prevent cascading failures with automatic fault detection
218
+ - ✅ **Idempotency** — SHA256-based deduplication of LLM retry quirks
219
+ - ✅ **Error Handling** — Machine-readable error codes with structured details → [ERRORS.md](docs/ERRORS.md)
220
+ - ✅ **Observability** — Structured logging, metrics, OpenTelemetry tracing → [OBSERVABILITY.md](docs/OBSERVABILITY.md)
221
+ - ✅ **Safety** — Subprocess isolation for untrusted code (zero crash blast radius)
222
+ - ✅ **Type Safety** — PEP 561 compliant with full mypy support
223
+ - ✅ **Resource Management** — Context managers for automatic cleanup
224
+ - ✅ **Tool Discovery** — Formal schema export (OpenAI, Anthropic, MCP formats)
225
+ - ✅ **Cancellation** — Cooperative cancellation with request-scoped deadlines
226
+
115
227
  ## Compatibility Matrix
116
228
 
229
+ Runs the same on macOS, Linux, and Windows — locally, serverside, and inside containers.
230
+
117
231
  | Component | Supported Versions | Notes |
118
232
  |-----------|-------------------|-------|
119
233
  | **Python** | 3.11, 3.12, 3.13 | Python 3.11+ required |
@@ -131,6 +245,19 @@ Each layer is **optional** and **configurable**. Mix and match what you need.
131
245
  - ✅ Anthropic Claude 3 (Opus, Sonnet, Haiku)
132
246
  - ✅ Local models (Ollama, LM Studio)
133
247
 
248
+ ## Developer Experience Highlights
249
+
250
+ **What makes CHUK Tool Processor easy to use:**
251
+
252
+ * **Auto-parsing**: XML (Claude), OpenAI `tool_calls`, direct JSON—all work automatically
253
+ * **One call**: `process()` handles multiple calls & formats in a single invocation
254
+ * **Auto-coercion**: Pydantic-powered argument cleanup (whitespace, type conversion, extra fields ignored)
255
+ * **Safe defaults**: timeouts, retries, caching toggles built-in
256
+ * **Observability in one line**: `setup_observability(...)` for traces + metrics
257
+ * **MCP in one call**: `setup_mcp_http_streamable|stdio|sse(...)` connects to remote tools instantly
258
+ * **Context managers**: `async with ToolProcessor() as p:` ensures automatic cleanup
259
+ * **Full type safety**: PEP 561 compliant—mypy, pyright, and IDEs get complete type information
260
+
134
261
  ## Quick Start
135
262
 
136
263
  ### Installation
@@ -143,19 +270,95 @@ pip install chuk-tool-processor
143
270
 
144
271
  # Using uv (recommended)
145
272
  uv pip install chuk-tool-processor
273
+ ```
146
274
 
147
- # Or from source
275
+ <details>
276
+ <summary><strong>Install from source or with extras</strong></summary>
277
+
278
+ ```bash
279
+ # From source
148
280
  git clone https://github.com/chrishayuk/chuk-tool-processor.git
149
281
  cd chuk-tool-processor
150
282
  uv pip install -e .
283
+
284
+ # With observability extras (OpenTelemetry + Prometheus)
285
+ pip install chuk-tool-processor[observability]
286
+
287
+ # With MCP extras
288
+ pip install chuk-tool-processor[mcp]
289
+
290
+ # All extras
291
+ pip install chuk-tool-processor[all]
151
292
  ```
152
293
 
294
+ </details>
295
+
296
+ <details>
297
+ <summary><strong>Type Checking Support (PEP 561 compliant)</strong></summary>
298
+
299
+ CHUK Tool Processor includes **full type checking support**:
300
+
301
+ ```python
302
+ # mypy, pyright, and IDEs get full type information!
303
+ from chuk_tool_processor import ToolProcessor, ToolCall, ToolResult
304
+
305
+ async with ToolProcessor() as processor:
306
+ # Full autocomplete and type checking
307
+ results: list[ToolResult] = await processor.process(llm_output)
308
+ tools: list[str] = await processor.list_tools()
309
+ ```
310
+
311
+ **Features:**
312
+ - ✅ `py.typed` marker for PEP 561 compliance
313
+ - ✅ Comprehensive type hints on all public APIs
314
+ - ✅ Works with mypy, pyright, pylance
315
+ - ✅ Full IDE autocomplete support
316
+
317
+ **No special mypy configuration needed** - just import and use!
318
+
319
+ </details>
320
+
153
321
  ## 60-Second Quick Start
154
322
 
155
- **Absolutely minimal example** See `examples/hello_tool.py`:
323
+ ### From raw LLM output to safe execution in 3 lines
324
+
325
+ ```python
326
+ from chuk_tool_processor import ToolProcessor, initialize
327
+
328
+ await initialize()
329
+ async with ToolProcessor() as p:
330
+ results = await p.process('<tool name="calculator" args=\'{"operation":"multiply","a":15,"b":23}\'/>')
331
+ ```
332
+
333
+ **Note:** This assumes you've registered a "calculator" tool. See complete example below.
334
+
335
+ ### Works with Both OpenAI and Anthropic (No Adapters Needed)
336
+
337
+ ```python
338
+ from chuk_tool_processor import ToolProcessor, register_tool, initialize
339
+
340
+ @register_tool(name="search")
341
+ class SearchTool:
342
+ async def execute(self, query: str) -> dict:
343
+ return {"results": [f"Found: {query}"]}
344
+
345
+ await initialize()
346
+ async with ToolProcessor() as p:
347
+ # OpenAI format
348
+ openai_response = {"tool_calls": [{"type": "function", "function": {"name": "search", "arguments": '{"query": "Python"}'}}]}
349
+
350
+ # Anthropic format
351
+ anthropic_response = '<tool name="search" args=\'{"query": "Python"}\'/>'
352
+
353
+ # Both work identically
354
+ results_openai = await p.process(openai_response)
355
+ results_anthropic = await p.process(anthropic_response)
356
+ ```
357
+
358
+ **Absolutely minimal example** → See `examples/01_getting_started/hello_tool.py`:
156
359
 
157
360
  ```bash
158
- python examples/hello_tool.py
361
+ python examples/01_getting_started/hello_tool.py
159
362
  ```
160
363
 
161
364
  Single file that demonstrates:
@@ -171,11 +374,10 @@ Copy-paste this into a file and run it:
171
374
 
172
375
  ```python
173
376
  import asyncio
174
- from chuk_tool_processor.core.processor import ToolProcessor
175
- from chuk_tool_processor.registry import initialize, register_tool
377
+ from chuk_tool_processor import ToolProcessor, tool
176
378
 
177
- # Step 1: Define a tool
178
- @register_tool(name="calculator")
379
+ # Step 1: Define a tool with the clean @tool decorator
380
+ @tool(name="calculator")
179
381
  class Calculator:
180
382
  async def execute(self, operation: str, a: float, b: float) -> dict:
181
383
  ops = {"add": a + b, "multiply": a * b, "subtract": a - b}
@@ -185,30 +387,223 @@ class Calculator:
185
387
 
186
388
  # Step 2: Process LLM output
187
389
  async def main():
188
- await initialize()
189
- processor = ToolProcessor()
390
+ # No initialize() needed - it auto-initializes!
190
391
 
191
- # Your LLM returned this tool call
192
- llm_output = '<tool name="calculator" args=\'{"operation": "multiply", "a": 15, "b": 23}\'/>'
392
+ # Use context manager for automatic cleanup
393
+ async with ToolProcessor() as processor:
394
+ # Your LLM returned this tool call
395
+ llm_output = '<tool name="calculator" args=\'{"operation": "multiply", "a": 15, "b": 23}\'/>'
193
396
 
194
- # Process it
195
- results = await processor.process(llm_output)
397
+ # Process it
398
+ results = await processor.process(llm_output)
196
399
 
197
- # Each result is a ToolExecutionResult with: tool, args, result, error, duration, cached
198
- # results[0].result contains the tool output
199
- # results[0].error contains any error message (None if successful)
200
- if results[0].error:
201
- print(f"Error: {results[0].error}")
202
- else:
203
- print(results[0].result) # {'result': 345}
400
+ # Each result is a ToolResult with: tool, result, error, duration, cached
401
+ if results[0].error:
402
+ print(f"Error: {results[0].error}")
403
+ else:
404
+ print(results[0].result) # {'result': 345}
405
+
406
+ # Processor automatically cleaned up!
204
407
 
205
408
  asyncio.run(main())
206
409
  ```
207
410
 
208
- **That's it.** You now have production-ready tool execution with timeouts, retries, and caching.
411
+ **That's it.** You now have production-ready tool execution with:
412
+ - ✅ Automatic timeouts, retries, and caching
413
+ - ✅ Clean resource management (context manager)
414
+ - ✅ Full type checking support
415
+ - ✅ Auto-initialization (no boilerplate!)
209
416
 
210
417
  > **Why not just use OpenAI tool calls?**
211
- > OpenAI's function calling is great for parsing, but you still need: parsing multiple formats (Anthropic XML, etc.), timeouts, retries, rate limits, caching, subprocess isolation, and connecting to external MCP servers. CHUK Tool Processor **is** that missing middle layer.
418
+ > OpenAI's function calling is great for parsing, but you still need: parsing multiple formats (Anthropic XML, etc.), timeouts, retries, rate limits, caching, subprocess isolation, connecting to external MCP servers, and **per-tool** policy control with cross-provider parsing and MCP fan-out. CHUK Tool Processor **is** that missing middle layer.
419
+
420
+ ### Enhanced Developer Experience
421
+
422
+ CHUK Tool Processor provides intuitive APIs and helpful error messages:
423
+
424
+ **1. Clean Decorator Syntax**
425
+ ```python
426
+ from chuk_tool_processor import tool
427
+
428
+ @tool(name="calculator") # Short and clean!
429
+ class Calculator:
430
+ async def execute(self, a: int, b: int) -> int:
431
+ return a + b
432
+ ```
433
+
434
+ **2. Auto-Initialization (No Boilerplate)**
435
+ ```python
436
+ from chuk_tool_processor import ToolProcessor
437
+
438
+ # No initialize() needed - it auto-initializes!
439
+ async with ToolProcessor() as p:
440
+ results = await p.process(llm_output)
441
+ ```
442
+
443
+ **3. Type-Safe Tool Discovery**
444
+ ```python
445
+ from chuk_tool_processor import get_default_registry, ToolInfo
446
+
447
+ registry = await get_default_registry()
448
+
449
+ # List all registered tools with clear, typed results
450
+ tools = await registry.list_tools()
451
+ for tool in tools: # Each tool is a ToolInfo object
452
+ print(f"{tool.namespace}:{tool.name}") # Clear attribute access!
453
+ # No more confusing tuple unpacking: (namespace, name) vs (name, namespace)?
454
+ ```
455
+
456
+ **4. Helpful Error Messages**
457
+ ```python
458
+ # Typo in tool name? Get helpful suggestions!
459
+ try:
460
+ await registry.get_tool_strict("calcuator", namespace="default")
461
+ except Exception as e:
462
+ print(e)
463
+ # Output:
464
+ # Tool 'calcuator' not found in namespace 'default'
465
+ #
466
+ # Did you mean: calculator?
467
+ #
468
+ # Available namespaces: default, math, mcp
469
+ #
470
+ # Tip: Use `await registry.list_tools()` to see all registered tools
471
+ ```
472
+
473
+ **5. Clean MCP Configuration**
474
+ ```python
475
+ from chuk_tool_processor.mcp import setup_mcp_stdio, MCPConfig, MCPServerConfig
476
+
477
+ # Clean Pydantic config object instead of 14+ parameters!
478
+ processor, manager = await setup_mcp_stdio(
479
+ config=MCPConfig(
480
+ servers=[MCPServerConfig(name="echo", command="uvx", args=["mcp-echo"])],
481
+ namespace="tools",
482
+ enable_caching=True,
483
+ cache_ttl=600,
484
+ )
485
+ )
486
+ ```
487
+
488
+ **Key improvements:**
489
+ - ✅ **`@tool` decorator**: Shorter, cleaner than `@register_tool`
490
+ - ✅ **Auto-initialization**: No need for explicit `initialize()` calls
491
+ - ✅ **Type-safe tool listing**: `ToolInfo` objects instead of confusing tuples
492
+ - ✅ **Helpful errors**: Fuzzy matching suggestions when tools aren't found
493
+ - ✅ **MCPConfig**: Clean Pydantic model instead of 14+ parameters
494
+ - ✅ **Better discoverability**: Clear guidance on how to explore available tools
495
+
496
+ ## Quick Decision Tree (Commit This to Memory)
497
+
498
+ ```
499
+ ╭──────────────────────────────────────────╮
500
+ │ Do you trust the code you're executing? │
501
+ │ ✅ Yes → InProcessStrategy │
502
+ │ ⚠️ No → IsolatedStrategy (sandboxed) │
503
+ │ │
504
+ │ Where do your tools live? │
505
+ │ 📦 Local → @tool decorator │
506
+ │ 🌐 Remote → setup_mcp_* with MCPConfig │
507
+ ╰──────────────────────────────────────────╯
508
+ ```
509
+
510
+ **That's all you need to pick the right pattern.**
511
+
512
+ ## Registry & Processor Lifecycle
513
+
514
+ Understanding the lifecycle helps you use CHUK Tool Processor correctly:
515
+
516
+ 1. **Auto-initialization** — Registry auto-initializes on first access (or call `await initialize()` explicitly)
517
+ 2. Create a **`ToolProcessor(...)`** (or use the one returned by `setup_mcp_*`)
518
+ 3. Use **`async with ToolProcessor() as p:`** to ensure cleanup
519
+ 4. **`setup_mcp_*`** returns `(processor, manager)` — reuse that `processor`
520
+ 5. If you need a custom registry, pass it explicitly to the strategy
521
+ 6. You rarely need `get_default_registry()` unless you're composing advanced setups
522
+
523
+ **New in this version:** The registry auto-initializes when you create a `ToolProcessor` or access `get_default_registry()`, so you can skip the explicit `initialize()` call in most cases!
524
+
525
+ ```python
526
+ # New simplified pattern (auto-initialization)
527
+ async with ToolProcessor() as p: # Auto-initializes on first use!
528
+ results = await p.process(llm_output)
529
+ # Processor automatically cleaned up on exit
530
+
531
+ # Traditional explicit pattern (still works)
532
+ await initialize() # Explicit initialization
533
+ async with ToolProcessor() as p:
534
+ results = await p.process(llm_output)
535
+ ```
536
+
537
+ ## Production Features by Example
538
+
539
+ ### Idempotency & Deduplication
540
+
541
+ Automatically deduplicate LLM retry quirks using SHA256-based idempotency keys:
542
+
543
+ ```python
544
+ from chuk_tool_processor import ToolProcessor, initialize
545
+
546
+ await initialize()
547
+ async with ToolProcessor(enable_caching=True, cache_ttl=300) as p:
548
+ # LLM retries the same call (common with streaming or errors)
549
+ call1 = '<tool name="search" args=\'{"query": "Python"}\'/>'
550
+ call2 = '<tool name="search" args=\'{"query": "Python"}\'/>' # Identical
551
+
552
+ results1 = await p.process(call1) # Executes
553
+ results2 = await p.process(call2) # Cache hit! (idempotency key match)
554
+
555
+ assert results1[0].cached == False
556
+ assert results2[0].cached == True
557
+ ```
558
+
559
+ ### Cancellation & Deadlines
560
+
561
+ Cooperative cancellation with request-scoped deadlines:
562
+
563
+ ```python
564
+ import asyncio
565
+ from chuk_tool_processor import ToolProcessor, initialize
566
+
567
+ async def main():
568
+ await initialize()
569
+ async with ToolProcessor(default_timeout=60.0) as p:
570
+ try:
571
+ # Hard deadline for the whole batch (e.g., user request budget)
572
+ async with asyncio.timeout(5.0):
573
+ async for event in p.astream('<tool name="slow_report" args=\'{"n": 1000000}\'/>'):
574
+ print("chunk:", event)
575
+ except TimeoutError:
576
+ print("Request cancelled: deadline exceeded")
577
+ # Processor automatically cancels the tool and cleans up
578
+
579
+ asyncio.run(main())
580
+ ```
581
+
582
+ ### Per-Tool Policy Overrides
583
+
584
+ Override timeouts, retries, and rate limits per tool:
585
+
586
+ ```python
587
+ from chuk_tool_processor import ToolProcessor, initialize
588
+
589
+ await initialize()
590
+ async with ToolProcessor(
591
+ default_timeout=30.0,
592
+ enable_retries=True,
593
+ max_retries=2,
594
+ enable_rate_limiting=True,
595
+ global_rate_limit=120, # 120 requests/min across all tools
596
+ tool_rate_limits={
597
+ "expensive_api": (5, 60), # 5 requests per 60 seconds
598
+ "fast_local": (1000, 60), # 1000 requests per 60 seconds
599
+ }
600
+ ) as p:
601
+ # Tools run with their specific policies
602
+ results = await p.process('''
603
+ <tool name="expensive_api" args='{"q":"abc"}'/>
604
+ <tool name="fast_local" args='{"data":"xyz"}'/>
605
+ ''')
606
+ ```
212
607
 
213
608
  ## Documentation Quick Reference
214
609
 
@@ -217,17 +612,19 @@ asyncio.run(main())
217
612
  | 📘 [CONFIGURATION.md](docs/CONFIGURATION.md) | **All config knobs & defaults**: ToolProcessor options, timeouts, retry policy, rate limits, circuit breakers, caching, environment variables |
218
613
  | 🚨 [ERRORS.md](docs/ERRORS.md) | **Error taxonomy**: All error codes, exception classes, error details structure, handling patterns, retryability guide |
219
614
  | 📊 [OBSERVABILITY.md](docs/OBSERVABILITY.md) | **Metrics & tracing**: OpenTelemetry setup, Prometheus metrics, spans reference, PromQL queries |
220
- | 🔌 [examples/hello_tool.py](examples/hello_tool.py) | **60-second starter**: Single-file, copy-paste-and-run example |
615
+ | 🔌 [examples/01_getting_started/hello_tool.py](examples/01_getting_started/hello_tool.py) | **60-second starter**: Single-file, copy-paste-and-run example |
221
616
  | 🎯 [examples/](examples/) | **20+ working examples**: MCP integration, OAuth flows, streaming, production patterns |
222
617
 
223
618
  ## Choose Your Path
224
619
 
620
+ **Use this when OpenAI/Claude tool calling is not enough** — because you need retries, caching, rate limits, subprocess isolation, or MCP integration.
621
+
225
622
  | Your Goal | What You Need | Where to Look |
226
623
  |-----------|---------------|---------------|
227
624
  | ☕ **Just process LLM tool calls** | Basic tool registration + processor | [60-Second Quick Start](#60-second-quick-start) |
228
625
  | 🔌 **Connect to external tools** | MCP integration (HTTP/STDIO/SSE) | [MCP Integration](#5-mcp-integration-external-tools) |
229
626
  | 🛡️ **Production deployment** | Timeouts, retries, rate limits, caching | [CONFIGURATION.md](docs/CONFIGURATION.md) |
230
- | 🔒 **Run untrusted code safely** | Subprocess isolation strategy | [Subprocess Strategy](#using-subprocess-strategy) |
627
+ | 🔒 **Run untrusted code safely** | Isolated strategy (subprocess) | [Isolated Strategy](#using-isolated-strategy) |
231
628
  | 📊 **Monitor and observe** | OpenTelemetry + Prometheus | [OBSERVABILITY.md](docs/OBSERVABILITY.md) |
232
629
  | 🌊 **Stream incremental results** | StreamingTool pattern | [StreamingTool](#streamingtool-real-time-results) |
233
630
  | 🚨 **Handle errors reliably** | Error codes & taxonomy | [ERRORS.md](docs/ERRORS.md) |
@@ -239,8 +636,7 @@ Here are the most common patterns you'll use:
239
636
  **Pattern 1: Local tools only**
240
637
  ```python
241
638
  import asyncio
242
- from chuk_tool_processor.core.processor import ToolProcessor
243
- from chuk_tool_processor.registry import initialize, register_tool
639
+ from chuk_tool_processor import ToolProcessor, register_tool, initialize
244
640
 
245
641
  @register_tool(name="my_tool")
246
642
  class MyTool:
@@ -249,20 +645,22 @@ class MyTool:
249
645
 
250
646
  async def main():
251
647
  await initialize()
252
- processor = ToolProcessor()
253
648
 
254
- llm_output = '<tool name="my_tool" args=\'{"arg": "hello"}\'/>'
255
- results = await processor.process(llm_output)
256
- print(results[0].result) # {'result': 'Processed: hello'}
649
+ async with ToolProcessor() as processor:
650
+ llm_output = '<tool name="my_tool" args=\'{"arg": "hello"}\'/>'
651
+ results = await processor.process(llm_output)
652
+ print(results[0].result) # {'result': 'Processed: hello'}
257
653
 
258
654
  asyncio.run(main())
259
655
  ```
260
656
 
657
+ <details>
658
+ <summary><strong>More patterns: MCP integration (local + remote tools)</strong></summary>
659
+
261
660
  **Pattern 2: Mix local + remote MCP tools (Notion)**
262
661
  ```python
263
662
  import asyncio
264
- from chuk_tool_processor.registry import initialize, register_tool
265
- from chuk_tool_processor.mcp import setup_mcp_http_streamable
663
+ from chuk_tool_processor import register_tool, initialize, setup_mcp_http_streamable
266
664
 
267
665
  @register_tool(name="local_calculator")
268
666
  class Calculator:
@@ -292,12 +690,49 @@ async def main():
292
690
  print(f"Local result: {results[0].result}")
293
691
  print(f"Notion result: {results[1].result}")
294
692
 
693
+ # Clean up
694
+ await manager.close()
695
+
295
696
  asyncio.run(main())
296
697
  ```
297
698
 
298
- See `examples/notion_oauth.py` for complete OAuth flow.
699
+ See `examples/04_mcp_integration/notion_oauth.py` for complete OAuth flow.
700
+
701
+ **Pattern 3: Local SQLite database via STDIO (New Clean API)**
702
+ ```python
703
+ import asyncio
704
+ from chuk_tool_processor.mcp import setup_mcp_stdio, MCPConfig, MCPServerConfig
705
+
706
+ async def main():
707
+ # NEW: Clean Pydantic config approach (recommended!)
708
+ processor, manager = await setup_mcp_stdio(
709
+ config=MCPConfig(
710
+ servers=[
711
+ MCPServerConfig(
712
+ name="sqlite",
713
+ command="uvx",
714
+ args=["mcp-server-sqlite", "--db-path", "./app.db"],
715
+ )
716
+ ],
717
+ namespace="db",
718
+ initialization_timeout=120.0, # First run downloads the package
719
+ enable_caching=True,
720
+ cache_ttl=600,
721
+ )
722
+ )
723
+
724
+ # Query your local database via MCP
725
+ results = await processor.process(
726
+ '<tool name="db.query" args=\'{"sql": "SELECT * FROM users LIMIT 10"}\'/>'
727
+ )
728
+ print(results[0].result)
729
+
730
+ asyncio.run(main())
731
+ ```
732
+
733
+ <details>
734
+ <summary><strong>Legacy approach (still works)</strong></summary>
299
735
 
300
- **Pattern 3: Local SQLite database via STDIO**
301
736
  ```python
302
737
  import asyncio
303
738
  import json
@@ -322,7 +757,7 @@ async def main():
322
757
  config_file="mcp_config.json",
323
758
  servers=["sqlite"],
324
759
  namespace="db",
325
- initialization_timeout=120.0 # First run downloads the package
760
+ initialization_timeout=120.0
326
761
  )
327
762
 
328
763
  # Query your local database via MCP
@@ -333,8 +768,11 @@ async def main():
333
768
 
334
769
  asyncio.run(main())
335
770
  ```
771
+ </details>
772
+
773
+ See `examples/04_mcp_integration/stdio_sqlite.py` for complete working example.
336
774
 
337
- See `examples/stdio_sqlite.py` for complete working example.
775
+ </details>
338
776
 
339
777
  ## Core Concepts
340
778
 
@@ -347,8 +785,10 @@ The **registry** is where you register tools for execution. Tools can be:
347
785
  - **StreamingTool** for real-time incremental results
348
786
  - **Functions** registered via `register_fn_tool()`
349
787
 
788
+ > **Note:** The registry is global, processors are scoped.
789
+
350
790
  ```python
351
- from chuk_tool_processor.registry import register_tool
791
+ from chuk_tool_processor import register_tool
352
792
  from chuk_tool_processor.models.validated_tool import ValidatedTool
353
793
  from pydantic import BaseModel, Field
354
794
 
@@ -374,18 +814,16 @@ class WeatherTool(ValidatedTool):
374
814
  | Strategy | Use Case | Trade-offs |
375
815
  |----------|----------|------------|
376
816
  | **InProcessStrategy** | Fast, trusted tools | Speed ✅, Isolation ❌ |
377
- | **SubprocessStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
817
+ | **IsolatedStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
378
818
 
379
819
  ```python
380
820
  import asyncio
381
- from chuk_tool_processor.core.processor import ToolProcessor
382
- from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
383
- from chuk_tool_processor.registry import get_default_registry
821
+ from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
384
822
 
385
823
  async def main():
386
824
  registry = await get_default_registry()
387
825
  processor = ToolProcessor(
388
- strategy=SubprocessStrategy(
826
+ strategy=IsolatedStrategy(
389
827
  registry=registry,
390
828
  max_workers=4,
391
829
  default_timeout=30.0
@@ -396,6 +834,8 @@ async def main():
396
834
  asyncio.run(main())
397
835
  ```
398
836
 
837
+ **Note:** `IsolatedStrategy` is an alias of `SubprocessStrategy` for backwards compatibility. Use `IsolatedStrategy` for clarity—it better communicates the security boundary intent.
838
+
399
839
  ### 3. Execution Wrappers (Middleware)
400
840
 
401
841
  **Wrappers** add production features as composable layers:
@@ -461,6 +901,8 @@ Connect to **remote tool servers** using the [Model Context Protocol](https://mo
461
901
 
462
902
  #### HTTP Streamable (⭐ Recommended for Cloud Services)
463
903
 
904
+ **Use for:** Cloud SaaS services (OAuth, long-running streams, resilient reconnects)
905
+
464
906
  Modern HTTP streaming transport for cloud-based MCP servers like Notion:
465
907
 
466
908
  ```python
@@ -489,8 +931,13 @@ results = await processor.process(
489
931
  )
490
932
  ```
491
933
 
934
+ <details>
935
+ <summary><strong>Other MCP Transports (STDIO for local tools, SSE for legacy)</strong></summary>
936
+
492
937
  #### STDIO (Best for Local/On-Device Tools)
493
938
 
939
+ **Use for:** Local/embedded tools and databases (SQLite, file systems, local services)
940
+
494
941
  For running local MCP servers as subprocesses—great for databases, file systems, and local tools:
495
942
 
496
943
  ```python
@@ -529,6 +976,8 @@ results = await processor.process(
529
976
 
530
977
  #### SSE (Legacy Support)
531
978
 
979
+ **Use for:** Legacy compatibility only. Prefer HTTP Streamable for new integrations.
980
+
532
981
  For backward compatibility with older MCP servers using Server-Sent Events:
533
982
 
534
983
  ```python
@@ -550,6 +999,8 @@ processor, manager = await setup_mcp_sse(
550
999
  )
551
1000
  ```
552
1001
 
1002
+ </details>
1003
+
553
1004
  **Transport Comparison:**
554
1005
 
555
1006
  | Transport | Use Case | Real Examples |
@@ -558,6 +1009,18 @@ processor, manager = await setup_mcp_sse(
558
1009
  | **STDIO** | Local tools, databases | SQLite (`mcp-server-sqlite`), Echo (`chuk-mcp-echo`) |
559
1010
  | **SSE** | Legacy cloud services | Atlassian (`mcp.atlassian.com`) |
560
1011
 
1012
+ **How MCP fits into the architecture:**
1013
+
1014
+ ```
1015
+ LLM Output
1016
+
1017
+ Tool Processor
1018
+
1019
+ ┌──────────────┬────────────────────┐
1020
+ │ Local Tools │ Remote Tools (MCP) │
1021
+ └──────────────┴────────────────────┘
1022
+ ```
1023
+
561
1024
  **Relationship with [chuk-mcp](https://github.com/chrishayuk/chuk-mcp):**
562
1025
  - `chuk-mcp` is a low-level MCP protocol client (handles transports, protocol negotiation)
563
1026
  - `chuk-tool-processor` wraps `chuk-mcp` to integrate external tools into your execution pipeline
@@ -571,7 +1034,7 @@ CHUK Tool Processor supports multiple patterns for defining tools:
571
1034
 
572
1035
  #### Simple Function-Based Tools
573
1036
  ```python
574
- from chuk_tool_processor.registry.auto_register import register_fn_tool
1037
+ from chuk_tool_processor import register_fn_tool
575
1038
  from datetime import datetime
576
1039
  from zoneinfo import ZoneInfo
577
1040
 
@@ -589,7 +1052,11 @@ register_fn_tool(get_current_time, namespace="utilities")
589
1052
  For production tools, use Pydantic validation:
590
1053
 
591
1054
  ```python
592
- @register_tool(name="weather")
1055
+ from chuk_tool_processor import tool
1056
+ from chuk_tool_processor.models import ValidatedTool
1057
+ from pydantic import BaseModel, Field
1058
+
1059
+ @tool(name="weather") # Clean @tool decorator
593
1060
  class WeatherTool(ValidatedTool):
594
1061
  class Arguments(BaseModel):
595
1062
  location: str = Field(..., description="City name")
@@ -603,14 +1070,28 @@ class WeatherTool(ValidatedTool):
603
1070
  return self.Result(temperature=22.5, conditions="Sunny")
604
1071
  ```
605
1072
 
1073
+ <details>
1074
+ <summary><strong>Alternative: Using @register_tool (still works)</strong></summary>
1075
+
1076
+ ```python
1077
+ from chuk_tool_processor import register_tool
1078
+
1079
+ @register_tool(name="weather") # Longer form, but identical functionality
1080
+ class WeatherTool(ValidatedTool):
1081
+ # ... same as above
1082
+ ```
1083
+ </details>
1084
+
606
1085
  #### StreamingTool (Real-time Results)
607
1086
 
608
1087
  For long-running operations that produce incremental results:
609
1088
 
610
1089
  ```python
1090
+ from chuk_tool_processor import tool
611
1091
  from chuk_tool_processor.models import StreamingTool
1092
+ from pydantic import BaseModel
612
1093
 
613
- @register_tool(name="file_processor")
1094
+ @tool(name="file_processor") # Clean @tool decorator
614
1095
  class FileProcessor(StreamingTool):
615
1096
  class Arguments(BaseModel):
616
1097
  file_path: str
@@ -629,17 +1110,26 @@ class FileProcessor(StreamingTool):
629
1110
 
630
1111
  ```python
631
1112
  import asyncio
632
- from chuk_tool_processor.core.processor import ToolProcessor
633
- from chuk_tool_processor.registry import initialize
1113
+ from chuk_tool_processor import ToolProcessor, initialize
634
1114
 
635
1115
  async def main():
636
1116
  await initialize()
637
1117
  processor = ToolProcessor()
638
- async for event in processor.astream('<tool name="file_processor" args=\'{"file_path":"README.md"}\'/>'):
639
- # 'event' is a streamed chunk (either your Result model instance or a dict)
640
- line = event["line"] if isinstance(event, dict) else getattr(event, "line", None)
641
- content = event["content"] if isinstance(event, dict) else getattr(event, "content", None)
642
- print(f"Line {line}: {content}")
1118
+
1119
+ # Stream can be cancelled by breaking or raising an exception
1120
+ try:
1121
+ async for event in processor.astream('<tool name="file_processor" args=\'{"file_path":"README.md"}\'/>'):
1122
+ # 'event' is a streamed chunk (either your Result model instance or a dict)
1123
+ line = event["line"] if isinstance(event, dict) else getattr(event, "line", None)
1124
+ content = event["content"] if isinstance(event, dict) else getattr(event, "content", None)
1125
+ print(f"Line {line}: {content}")
1126
+
1127
+ # Example: cancel after 100 lines
1128
+ if line and line > 100:
1129
+ break # Cleanup happens automatically
1130
+ except asyncio.CancelledError:
1131
+ # Stream cleanup is automatic even on cancellation
1132
+ pass
643
1133
 
644
1134
  asyncio.run(main())
645
1135
  ```
@@ -648,23 +1138,32 @@ asyncio.run(main())
648
1138
 
649
1139
  #### Basic Usage
650
1140
 
651
- Call `await initialize()` once at startup to load your registry.
1141
+ Call `await initialize()` once at startup to load your registry. Use context managers for automatic cleanup:
652
1142
 
653
1143
  ```python
654
1144
  import asyncio
655
- from chuk_tool_processor.core.processor import ToolProcessor
656
- from chuk_tool_processor.registry import initialize
1145
+ from chuk_tool_processor import ToolProcessor, initialize
657
1146
 
658
1147
  async def main():
659
1148
  await initialize()
660
- processor = ToolProcessor()
661
- llm_output = '<tool name="calculator" args=\'{"operation":"add","a":2,"b":3}\'/>'
662
- results = await processor.process(llm_output)
663
- for result in results:
664
- if result.error:
665
- print(f"Error: {result.error}")
666
- else:
667
- print(f"Success: {result.result}")
1149
+
1150
+ # Context manager automatically handles cleanup
1151
+ async with ToolProcessor() as processor:
1152
+ # Discover available tools
1153
+ tools = await processor.list_tools()
1154
+ print(f"Available tools: {tools}")
1155
+
1156
+ # Process LLM output
1157
+ llm_output = '<tool name="calculator" args=\'{"operation":"add","a":2,"b":3}\'/>'
1158
+ results = await processor.process(llm_output)
1159
+
1160
+ for result in results:
1161
+ if result.error:
1162
+ print(f"Error: {result.error}")
1163
+ else:
1164
+ print(f"Success: {result.result}")
1165
+
1166
+ # Processor automatically cleaned up here!
668
1167
 
669
1168
  asyncio.run(main())
670
1169
  ```
@@ -672,21 +1171,32 @@ asyncio.run(main())
672
1171
  #### Production Configuration
673
1172
 
674
1173
  ```python
675
- from chuk_tool_processor.core.processor import ToolProcessor
1174
+ from chuk_tool_processor import ToolProcessor, initialize
1175
+ import asyncio
676
1176
 
677
- processor = ToolProcessor(
678
- # Execution settings
679
- default_timeout=30.0,
680
- max_concurrency=20,
1177
+ async def main():
1178
+ await initialize()
681
1179
 
682
- # Production features
683
- enable_caching=True,
684
- cache_ttl=600,
685
- enable_rate_limiting=True,
686
- global_rate_limit=100,
687
- enable_retries=True,
688
- max_retries=3
689
- )
1180
+ # Use context manager with production config
1181
+ async with ToolProcessor(
1182
+ # Execution settings
1183
+ default_timeout=30.0,
1184
+ max_concurrency=20,
1185
+
1186
+ # Production features
1187
+ enable_caching=True,
1188
+ cache_ttl=600,
1189
+ enable_rate_limiting=True,
1190
+ global_rate_limit=100,
1191
+ enable_retries=True,
1192
+ max_retries=3
1193
+ ) as processor:
1194
+ # Use processor...
1195
+ results = await processor.process(llm_output)
1196
+
1197
+ # Automatic cleanup on exit
1198
+
1199
+ asyncio.run(main())
690
1200
  ```
691
1201
 
692
1202
  ### Advanced Production Features
@@ -698,7 +1208,7 @@ Beyond basic configuration, CHUK Tool Processor includes several advanced featur
698
1208
  Prevent cascading failures by automatically opening circuits for failing tools:
699
1209
 
700
1210
  ```python
701
- from chuk_tool_processor.core.processor import ToolProcessor
1211
+ from chuk_tool_processor import ToolProcessor
702
1212
 
703
1213
  processor = ToolProcessor(
704
1214
  enable_circuit_breaker=True,
@@ -740,8 +1250,8 @@ assert call1.idempotency_key == call2.idempotency_key
740
1250
 
741
1251
  # Used automatically by caching layer
742
1252
  processor = ToolProcessor(enable_caching=True)
743
- results1 = await processor.execute([call1]) # Executes
744
- results2 = await processor.execute([call2]) # Cache hit!
1253
+ results1 = await processor.process([call1]) # Executes
1254
+ results2 = await processor.process([call2]) # Cache hit!
745
1255
  ```
746
1256
 
747
1257
  **Benefits:**
@@ -749,6 +1259,8 @@ results2 = await processor.execute([call2]) # Cache hit!
749
1259
  - Deterministic cache keys
750
1260
  - No manual key management needed
751
1261
 
1262
+ **Cache scope:** In-memory per-process by default. Cache backend is pluggable—see [CONFIGURATION.md](docs/CONFIGURATION.md) for custom cache backends.
1263
+
752
1264
  #### Tool Schema Export
753
1265
 
754
1266
  Export tool definitions to multiple formats for LLM prompting:
@@ -795,7 +1307,9 @@ mcp_format = spec.to_mcp() # For MCP servers
795
1307
 
796
1308
  #### Machine-Readable Error Codes
797
1309
 
798
- Structured error handling with error codes for programmatic responses:
1310
+ Structured error handling with error codes for programmatic responses.
1311
+
1312
+ **Error Contract:** Every error includes a machine-readable code, human-readable message, and structured details:
799
1313
 
800
1314
  ```python
801
1315
  from chuk_tool_processor.core.exceptions import (
@@ -877,22 +1391,20 @@ result = await tool.execute(**llm_output)
877
1391
 
878
1392
  ## Advanced Topics
879
1393
 
880
- ### Using Subprocess Strategy
1394
+ ### Using Isolated Strategy
881
1395
 
882
- Use `SubprocessStrategy` when running untrusted, third-party, or potentially unsafe code that shouldn't share the same process as your main app.
1396
+ Use `IsolatedStrategy` when running untrusted, third-party, or potentially unsafe code that shouldn't share the same process as your main app.
883
1397
 
884
1398
  For isolation and safety when running untrusted code:
885
1399
 
886
1400
  ```python
887
1401
  import asyncio
888
- from chuk_tool_processor.core.processor import ToolProcessor
889
- from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
890
- from chuk_tool_processor.registry import get_default_registry
1402
+ from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
891
1403
 
892
1404
  async def main():
893
1405
  registry = await get_default_registry()
894
1406
  processor = ToolProcessor(
895
- strategy=SubprocessStrategy(
1407
+ strategy=IsolatedStrategy(
896
1408
  registry=registry,
897
1409
  max_workers=4,
898
1410
  default_timeout=30.0
@@ -903,6 +1415,10 @@ async def main():
903
1415
  asyncio.run(main())
904
1416
  ```
905
1417
 
1418
+ > **Security & Isolation — Threat Model**
1419
+ >
1420
+ > Untrusted tool code runs in subprocesses; faults and crashes don't bring down your app. **Zero crash blast radius.** For hard CPU/RAM/network limits, run the processor inside a container with `--cpus`, `--memory`, and egress filtering. Secrets are never injected by default—pass them explicitly via tool arguments or scoped environment variables.
1421
+
906
1422
  ### Real-World MCP Examples
907
1423
 
908
1424
  #### Example 1: Notion Integration with OAuth
@@ -912,7 +1428,7 @@ Complete OAuth flow connecting to Notion's MCP server:
912
1428
  ```python
913
1429
  from chuk_tool_processor.mcp import setup_mcp_http_streamable
914
1430
 
915
- # After completing OAuth flow (see examples/notion_oauth.py for full flow)
1431
+ # After completing OAuth flow (see examples/04_mcp_integration/notion_oauth.py for full flow)
916
1432
  processor, manager = await setup_mcp_http_streamable(
917
1433
  servers=[{
918
1434
  "name": "notion",
@@ -933,6 +1449,9 @@ results = await processor.process(
933
1449
  )
934
1450
  ```
935
1451
 
1452
+ <details>
1453
+ <summary><strong>Click to expand more MCP examples (SQLite, Echo Server)</strong></summary>
1454
+
936
1455
  #### Example 2: Local SQLite Database Access
937
1456
 
938
1457
  Run SQLite MCP server locally for database operations:
@@ -1004,10 +1523,15 @@ results = await processor.process(
1004
1523
  )
1005
1524
  ```
1006
1525
 
1007
- See `examples/notion_oauth.py`, `examples/stdio_sqlite.py`, and `examples/stdio_echo.py` for complete working implementations.
1526
+ </details>
1527
+
1528
+ See `examples/04_mcp_integration/notion_oauth.py`, `examples/04_mcp_integration/stdio_sqlite.py`, and `examples/04_mcp_integration/stdio_echo.py` for complete working implementations.
1008
1529
 
1009
1530
  #### OAuth Token Refresh
1010
1531
 
1532
+ <details>
1533
+ <summary><strong>Click to expand OAuth token refresh guide</strong></summary>
1534
+
1011
1535
  For MCP servers that use OAuth authentication, CHUK Tool Processor supports automatic token refresh when access tokens expire. This prevents your tools from failing due to expired tokens during long-running sessions.
1012
1536
 
1013
1537
  **How it works:**
@@ -1076,7 +1600,9 @@ processor, manager = await setup_mcp_sse(
1076
1600
  - Token refresh is attempted only once per tool call (no infinite retry loops)
1077
1601
  - After successful refresh, the updated headers are used for all subsequent calls
1078
1602
 
1079
- See `examples/notion_oauth.py` for a complete OAuth 2.1 implementation with PKCE and automatic token refresh.
1603
+ See `examples/04_mcp_integration/notion_oauth.py` for a complete OAuth 2.1 implementation with PKCE and automatic token refresh.
1604
+
1605
+ </details>
1080
1606
 
1081
1607
  ### Observability
1082
1608
 
@@ -1145,6 +1671,9 @@ asyncio.run(main())
1145
1671
 
1146
1672
  #### OpenTelemetry & Prometheus (Drop-in Observability)
1147
1673
 
1674
+ <details>
1675
+ <summary><strong>Click to expand complete observability guide</strong></summary>
1676
+
1148
1677
  **3-Line Setup:**
1149
1678
 
1150
1679
  ```python
@@ -1208,8 +1737,7 @@ uv pip install chuk-tool-processor --group observability
1208
1737
  ```python
1209
1738
  import asyncio
1210
1739
  from chuk_tool_processor.observability import setup_observability
1211
- from chuk_tool_processor.core.processor import ToolProcessor
1212
- from chuk_tool_processor.registry import initialize, register_tool
1740
+ from chuk_tool_processor import ToolProcessor, initialize, register_tool
1213
1741
 
1214
1742
  @register_tool(name="weather_api")
1215
1743
  class WeatherTool:
@@ -1427,7 +1955,7 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://datadog-agent:4317
1427
1955
  - Testing observability features
1428
1956
  - Environment variable configuration
1429
1957
 
1430
- 🎯 **Working Example**: See `examples/observability_demo.py` for a complete demonstration with retries, caching, and circuit breakers
1958
+ 🎯 **Working Example**: See `examples/02_production_features/observability_demo.py` for a complete demonstration with retries, caching, and circuit breakers
1431
1959
 
1432
1960
  **Benefits**
1433
1961
 
@@ -1438,6 +1966,8 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://datadog-agent:4317
1438
1966
  ✅ **Optional** - Gracefully degrades if packages not installed
1439
1967
  ✅ **Zero-overhead** - No performance impact when disabled
1440
1968
 
1969
+ </details>
1970
+
1441
1971
  ### Error Handling
1442
1972
 
1443
1973
  ```python
@@ -1455,8 +1985,7 @@ for result in results:
1455
1985
 
1456
1986
  ```python
1457
1987
  import pytest
1458
- from chuk_tool_processor.core.processor import ToolProcessor
1459
- from chuk_tool_processor.registry import initialize
1988
+ from chuk_tool_processor import ToolProcessor, initialize
1460
1989
 
1461
1990
  @pytest.mark.asyncio
1462
1991
  async def test_calculator():
@@ -1470,6 +1999,40 @@ async def test_calculator():
1470
1999
  assert results[0].result["result"] == 8
1471
2000
  ```
1472
2001
 
2002
+ **Fake tool pattern for testing:**
2003
+
2004
+ ```python
2005
+ import pytest
2006
+ from chuk_tool_processor import ToolProcessor, register_tool, initialize
2007
+
2008
+ @register_tool(name="fake_tool")
2009
+ class FakeTool:
2010
+ """No-op tool for testing processor behavior."""
2011
+ call_count = 0
2012
+
2013
+ async def execute(self, **kwargs) -> dict:
2014
+ FakeTool.call_count += 1
2015
+ return {"called": True, "args": kwargs}
2016
+
2017
+ @pytest.mark.asyncio
2018
+ async def test_processor_with_fake_tool():
2019
+ await initialize()
2020
+ processor = ToolProcessor()
2021
+
2022
+ # Reset counter
2023
+ FakeTool.call_count = 0
2024
+
2025
+ # Execute fake tool
2026
+ results = await processor.process(
2027
+ '<tool name="fake_tool" args=\'{"test_arg": "value"}\'/>'
2028
+ )
2029
+
2030
+ # Assert behavior
2031
+ assert FakeTool.call_count == 1
2032
+ assert results[0].result["called"] is True
2033
+ assert results[0].result["args"]["test_arg"] == "value"
2034
+ ```
2035
+
1473
2036
  ## Configuration
1474
2037
 
1475
2038
  ### Timeout Configuration
@@ -1480,6 +2043,7 @@ CHUK Tool Processor uses a unified timeout configuration system that applies to
1480
2043
  from chuk_tool_processor.mcp.transport import TimeoutConfig
1481
2044
 
1482
2045
  # Create custom timeout configuration
2046
+ # (Defaults are: connect=30, operation=30, quick=5, shutdown=2)
1483
2047
  timeout_config = TimeoutConfig(
1484
2048
  connect=30.0, # Connection establishment, initialization, session discovery
1485
2049
  operation=30.0, # Normal operations (tool calls, listing tools/resources/prompts)
@@ -1607,7 +2171,7 @@ CHUK Tool Processor provides multiple layers of safety:
1607
2171
  | Concern | Protection | Configuration |
1608
2172
  |---------|------------|---------------|
1609
2173
  | **Timeouts** | Every tool has a timeout | `default_timeout=30.0` |
1610
- | **Process Isolation** | Run tools in separate processes | `strategy=SubprocessStrategy()` |
2174
+ | **Process Isolation** | Run tools in separate processes | `strategy=IsolatedStrategy()` |
1611
2175
  | **Rate Limiting** | Prevent abuse and API overuse | `enable_rate_limiting=True` |
1612
2176
  | **Input Validation** | Pydantic validation on arguments | Use `ValidatedTool` |
1613
2177
  | **Error Containment** | Failures don't crash the processor | Built-in exception handling |
@@ -1619,13 +2183,58 @@ CHUK Tool Processor provides multiple layers of safety:
1619
2183
  - **Resource Limits**: For hard CPU/memory caps, use OS-level controls (cgroups on Linux, Job Objects on Windows, or Docker resource limits).
1620
2184
  - **Secrets**: Never injected automatically. Pass secrets explicitly via tool arguments or environment variables, and prefer scoped env vars for subprocess tools to minimize exposure.
1621
2185
 
2186
+ #### OS-Level Hardening
2187
+
2188
+ For production deployments, add these hardening measures:
2189
+
2190
+ | Concern | Docker/Container Solution | Direct Example |
2191
+ |---------|--------------------------|----------------|
2192
+ | **CPU/RAM caps** | `--cpus`, `--memory` flags | `docker run --cpus="1.5" --memory="512m" myapp` |
2193
+ | **Network egress** | Deny-by-default with firewall rules | `--network=none` or custom network with egress filtering |
2194
+ | **Filesystem** | Read-only root + writable scratch | `--read-only --tmpfs /tmp:rw,size=100m` |
2195
+
2196
+ **Example: Run processor in locked-down container**
2197
+
2198
+ ```bash
2199
+ # Dockerfile
2200
+ FROM python:3.11-slim
2201
+ WORKDIR /app
2202
+ COPY requirements.txt .
2203
+ RUN pip install -r requirements.txt --no-cache-dir
2204
+ COPY . .
2205
+ USER nobody # Run as non-root
2206
+ CMD ["python", "app.py"]
2207
+
2208
+ # Run with resource limits and network restrictions
2209
+ docker run \
2210
+ --cpus="2" \
2211
+ --memory="1g" \
2212
+ --memory-swap="1g" \
2213
+ --read-only \
2214
+ --tmpfs /tmp:rw,size=200m,mode=1777 \
2215
+ --network=custom-net \
2216
+ --cap-drop=ALL \
2217
+ myapp:latest
2218
+ ```
2219
+
2220
+ **Network egress controls (deny-by-default)**
2221
+
2222
+ ```bash
2223
+ # Create restricted network with no internet access (for local-only tools)
2224
+ docker network create --internal restricted-net
2225
+
2226
+ # Or use iptables for per-tool CIDR allowlists
2227
+ iptables -A OUTPUT -d 10.0.0.0/8 -j ACCEPT # Allow private ranges
2228
+ iptables -A OUTPUT -d 172.16.0.0/12 -j ACCEPT
2229
+ iptables -A OUTPUT -d 192.168.0.0/16 -j ACCEPT
2230
+ iptables -A OUTPUT -j DROP # Deny everything else
2231
+ ```
2232
+
1622
2233
  Example security-focused setup for untrusted code:
1623
2234
 
1624
2235
  ```python
1625
2236
  import asyncio
1626
- from chuk_tool_processor.core.processor import ToolProcessor
1627
- from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
1628
- from chuk_tool_processor.registry import get_default_registry
2237
+ from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
1629
2238
 
1630
2239
  async def create_secure_processor():
1631
2240
  # Maximum isolation for untrusted code
@@ -1633,7 +2242,7 @@ async def create_secure_processor():
1633
2242
  registry = await get_default_registry()
1634
2243
 
1635
2244
  processor = ToolProcessor(
1636
- strategy=SubprocessStrategy(
2245
+ strategy=IsolatedStrategy(
1637
2246
  registry=registry,
1638
2247
  max_workers=4,
1639
2248
  default_timeout=10.0
@@ -1651,6 +2260,25 @@ async def create_secure_processor():
1651
2260
  # - Use read-only filesystems where possible
1652
2261
  ```
1653
2262
 
2263
+ ## Design Goals & Non-Goals
2264
+
2265
+ **What CHUK Tool Processor does:**
2266
+ - ✅ Parse tool calls from any LLM format (XML, OpenAI, JSON)
2267
+ - ✅ Execute tools with production policies (timeouts, retries, rate limits, caching)
2268
+ - ✅ Isolate untrusted code in subprocesses
2269
+ - ✅ Connect to remote tool servers via MCP (HTTP/STDIO/SSE)
2270
+ - ✅ Provide composable execution layers (strategies + wrappers)
2271
+ - ✅ Export tool schemas for LLM prompting
2272
+
2273
+ **What CHUK Tool Processor explicitly does NOT do:**
2274
+ - ❌ Manage conversations or chat history
2275
+ - ❌ Provide prompt engineering or prompt templates
2276
+ - ❌ Bundle an LLM client (bring your own OpenAI/Anthropic/local)
2277
+ - ❌ Implement agent frameworks or chains
2278
+ - ❌ Make decisions about which tools to call
2279
+
2280
+ **Why this matters:** CHUK Tool Processor stays focused on reliable tool execution. It's a building block, not a framework. This makes it composable with any LLM application architecture.
2281
+
1654
2282
  ## Architecture Principles
1655
2283
 
1656
2284
  1. **Composability**: Stack strategies and wrappers like middleware
@@ -1664,29 +2292,26 @@ async def create_secure_processor():
1664
2292
  Check out the [`examples/`](examples/) directory for complete working examples:
1665
2293
 
1666
2294
  ### Getting Started
1667
- - **60-second hello**: `examples/hello_tool.py` - Absolute minimal example (copy-paste-run)
1668
- - **Quick start**: `examples/quickstart_demo.py` - Basic tool registration and execution
1669
- - **Execution strategies**: `examples/execution_strategies_demo.py` - InProcess vs Subprocess
1670
- - **Production wrappers**: `examples/wrappers_demo.py` - Caching, retries, rate limiting
1671
- - **Streaming tools**: `examples/streaming_demo.py` - Real-time incremental results
1672
- - **Streaming tool calls**: `examples/streaming_tool_calls_demo.py` - Handle partial tool calls from streaming LLMs
1673
- - **Schema helper**: `examples/schema_helper_demo.py` - Auto-generate schemas from typed tools (Pydantic → OpenAI/Anthropic/MCP)
1674
- - **Observability**: `examples/observability_demo.py` - OpenTelemetry + Prometheus integration
2295
+ - **60-second hello**: `examples/01_getting_started/hello_tool.py` - Absolute minimal example (copy-paste-run)
2296
+ - **Quick start**: `examples/01_getting_started/quickstart_demo.py` - Basic tool registration and execution
2297
+ - **Execution strategies**: `examples/01_getting_started/execution_strategies_demo.py` - InProcess vs Subprocess
2298
+ - **Production wrappers**: `examples/02_production_features/wrappers_demo.py` - Caching, retries, rate limiting
2299
+ - **Streaming tools**: `examples/03_streaming/streaming_demo.py` - Real-time incremental results
2300
+ - **Streaming tool calls**: `examples/03_streaming/streaming_tool_calls_demo.py` - Handle partial tool calls from streaming LLMs
2301
+ - **Schema helper**: `examples/05_schema_and_types/schema_helper_demo.py` - Auto-generate schemas from typed tools (Pydantic → OpenAI/Anthropic/MCP)
2302
+ - **Observability**: `examples/02_production_features/observability_demo.py` - OpenTelemetry + Prometheus integration
1675
2303
 
1676
2304
  ### MCP Integration (Real-World)
1677
- - **Notion + OAuth**: `examples/notion_oauth.py` - Complete OAuth 2.1 flow with HTTP Streamable
2305
+ - **Notion + OAuth**: `examples/04_mcp_integration/notion_oauth.py` - Complete OAuth 2.1 flow with HTTP Streamable
1678
2306
  - Shows: Authorization Server discovery, client registration, PKCE flow, token exchange
1679
- - **SQLite Local**: `examples/stdio_sqlite.py` - Local database access via STDIO
2307
+ - **SQLite Local**: `examples/04_mcp_integration/stdio_sqlite.py` - Local database access via STDIO
1680
2308
  - Shows: Command/args passing, environment variables, file paths, initialization timeouts
1681
- - **Echo Server**: `examples/stdio_echo.py` - Minimal STDIO transport example
2309
+ - **Echo Server**: `examples/04_mcp_integration/stdio_echo.py` - Minimal STDIO transport example
1682
2310
  - Shows: Simplest possible MCP integration for testing
1683
- - **Atlassian + OAuth**: `examples/atlassian_sse.py` - OAuth with SSE transport (legacy)
2311
+ - **Atlassian + OAuth**: `examples/04_mcp_integration/atlassian_sse.py` - OAuth with SSE transport (legacy)
1684
2312
 
1685
2313
  ### Advanced MCP
1686
- - **HTTP Streamable**: `examples/mcp_http_streamable_example.py`
1687
- - **STDIO**: `examples/mcp_stdio_example.py`
1688
- - **SSE**: `examples/mcp_sse_example.py`
1689
- - **Plugin system**: `examples/plugins_builtins_demo.py`, `examples/plugins_custom_parser_demo.py`
2314
+ - **Plugin system**: `examples/06_plugins/plugins_builtins_demo.py`, `examples/06_plugins/plugins_custom_parser_demo.py`
1690
2315
 
1691
2316
  ## FAQ
1692
2317
 
@@ -1711,18 +2336,20 @@ A: Use pytest with `@pytest.mark.asyncio`. See [Testing Tools](#testing-tools) f
1711
2336
  **Q: Does this work with streaming LLM responses?**
1712
2337
  A: Yes—as tool calls appear in the stream, extract and process them. The processor handles partial/incremental tool call lists.
1713
2338
 
1714
- **Q: What's the difference between InProcess and Subprocess strategies?**
1715
- A: InProcess is faster (same process), Subprocess is safer (isolated process). Use InProcess for trusted code, Subprocess for untrusted.
2339
+ **Q: What's the difference between InProcess and Isolated strategies?**
2340
+ A: InProcess is faster (same process), Isolated is safer (separate subprocess). Use InProcess for trusted code, Isolated for untrusted.
1716
2341
 
1717
2342
  ## Comparison with Other Tools
1718
2343
 
1719
2344
  | Feature | chuk-tool-processor | LangChain Tools | OpenAI Tools | MCP SDK |
1720
2345
  |---------|-------------------|-----------------|--------------|---------|
1721
2346
  | **Async-native** | ✅ | ⚠️ Partial | ✅ | ✅ |
1722
- | **Process isolation** | ✅ SubprocessStrategy | ❌ | ❌ | ⚠️ |
2347
+ | **Process isolation** | ✅ IsolatedStrategy | ❌ | ❌ | ⚠️ |
1723
2348
  | **Built-in retries** | ✅ | ❌ † | ❌ | ❌ |
1724
2349
  | **Rate limiting** | ✅ | ❌ † | ⚠️ ‡ | ❌ |
1725
2350
  | **Caching** | ✅ | ⚠️ † | ❌ ‡ | ❌ |
2351
+ | **Idempotency & de-dup** | ✅ SHA256 keys | ❌ | ❌ | ❌ |
2352
+ | **Per-tool policies** | ✅ (timeouts/retries/limits) | ⚠️ | ❌ | ❌ |
1726
2353
  | **Multiple parsers** | ✅ (XML, OpenAI, JSON) | ⚠️ | ✅ | ✅ |
1727
2354
  | **Streaming tools** | ✅ | ⚠️ | ⚠️ | ✅ |
1728
2355
  | **MCP integration** | ✅ All transports | ❌ | ❌ | ✅ (protocol only) |
@@ -1799,6 +2426,25 @@ For detailed release documentation, see:
1799
2426
  - **[RELEASING.md](RELEASING.md)** - Complete release process guide
1800
2427
  - **[docs/CI-CD.md](docs/CI-CD.md)** - Full CI/CD pipeline documentation
1801
2428
 
2429
+ ## Stability & Versioning
2430
+
2431
+ CHUK Tool Processor follows **[Semantic Versioning 2.0.0](https://semver.org/)** for predictable upgrades:
2432
+
2433
+ * **Breaking changes** = **major** version bump (e.g., 1.x → 2.0)
2434
+ * **New features** (backward-compatible) = **minor** version bump (e.g., 1.2 → 1.3)
2435
+ * **Bug fixes** (backward-compatible) = **patch** version bump (e.g., 1.2.3 → 1.2.4)
2436
+
2437
+ **Public API surface**: Everything exported via the package root (`from chuk_tool_processor import ...`) is considered public API and follows semver guarantees.
2438
+
2439
+ **Deprecation policy**: Deprecated APIs will:
2440
+ 1. Log a warning for **one minor release**
2441
+ 2. Be removed in the **next major release**
2442
+
2443
+ **Upgrading safely**:
2444
+ * Patch and minor updates are **safe to deploy** without code changes
2445
+ * Major updates may require migration—see release notes
2446
+ * Pin to `chuk-tool-processor~=1.2` for minor updates only, or `chuk-tool-processor==1.2.3` for exact versions
2447
+
1802
2448
  ## Contributing & Support
1803
2449
 
1804
2450
  - **GitHub**: [chrishayuk/chuk-tool-processor](https://github.com/chrishayuk/chuk-tool-processor)