chuk-tool-processor 0.6.4__py3-none-any.whl → 0.9.7__py3-none-any.whl

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.

Potentially problematic release.


This version of chuk-tool-processor might be problematic. Click here for more details.

Files changed (66) hide show
  1. chuk_tool_processor/core/__init__.py +32 -1
  2. chuk_tool_processor/core/exceptions.py +225 -13
  3. chuk_tool_processor/core/processor.py +135 -104
  4. chuk_tool_processor/execution/strategies/__init__.py +6 -0
  5. chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
  6. chuk_tool_processor/execution/strategies/subprocess_strategy.py +202 -206
  7. chuk_tool_processor/execution/tool_executor.py +82 -84
  8. chuk_tool_processor/execution/wrappers/__init__.py +42 -0
  9. chuk_tool_processor/execution/wrappers/caching.py +150 -116
  10. chuk_tool_processor/execution/wrappers/circuit_breaker.py +370 -0
  11. chuk_tool_processor/execution/wrappers/rate_limiting.py +76 -43
  12. chuk_tool_processor/execution/wrappers/retry.py +116 -78
  13. chuk_tool_processor/logging/__init__.py +23 -17
  14. chuk_tool_processor/logging/context.py +40 -45
  15. chuk_tool_processor/logging/formatter.py +22 -21
  16. chuk_tool_processor/logging/helpers.py +28 -42
  17. chuk_tool_processor/logging/metrics.py +13 -15
  18. chuk_tool_processor/mcp/__init__.py +8 -12
  19. chuk_tool_processor/mcp/mcp_tool.py +158 -114
  20. chuk_tool_processor/mcp/register_mcp_tools.py +22 -22
  21. chuk_tool_processor/mcp/setup_mcp_http_streamable.py +57 -17
  22. chuk_tool_processor/mcp/setup_mcp_sse.py +57 -17
  23. chuk_tool_processor/mcp/setup_mcp_stdio.py +11 -11
  24. chuk_tool_processor/mcp/stream_manager.py +333 -276
  25. chuk_tool_processor/mcp/transport/__init__.py +22 -29
  26. chuk_tool_processor/mcp/transport/base_transport.py +180 -44
  27. chuk_tool_processor/mcp/transport/http_streamable_transport.py +505 -325
  28. chuk_tool_processor/mcp/transport/models.py +100 -0
  29. chuk_tool_processor/mcp/transport/sse_transport.py +607 -276
  30. chuk_tool_processor/mcp/transport/stdio_transport.py +597 -116
  31. chuk_tool_processor/models/__init__.py +21 -1
  32. chuk_tool_processor/models/execution_strategy.py +16 -21
  33. chuk_tool_processor/models/streaming_tool.py +28 -25
  34. chuk_tool_processor/models/tool_call.py +49 -31
  35. chuk_tool_processor/models/tool_export_mixin.py +22 -8
  36. chuk_tool_processor/models/tool_result.py +40 -77
  37. chuk_tool_processor/models/tool_spec.py +350 -0
  38. chuk_tool_processor/models/validated_tool.py +36 -18
  39. chuk_tool_processor/observability/__init__.py +30 -0
  40. chuk_tool_processor/observability/metrics.py +312 -0
  41. chuk_tool_processor/observability/setup.py +105 -0
  42. chuk_tool_processor/observability/tracing.py +345 -0
  43. chuk_tool_processor/plugins/__init__.py +1 -1
  44. chuk_tool_processor/plugins/discovery.py +11 -11
  45. chuk_tool_processor/plugins/parsers/__init__.py +1 -1
  46. chuk_tool_processor/plugins/parsers/base.py +1 -2
  47. chuk_tool_processor/plugins/parsers/function_call_tool.py +13 -8
  48. chuk_tool_processor/plugins/parsers/json_tool.py +4 -3
  49. chuk_tool_processor/plugins/parsers/openai_tool.py +12 -7
  50. chuk_tool_processor/plugins/parsers/xml_tool.py +4 -4
  51. chuk_tool_processor/registry/__init__.py +12 -12
  52. chuk_tool_processor/registry/auto_register.py +22 -30
  53. chuk_tool_processor/registry/decorators.py +127 -129
  54. chuk_tool_processor/registry/interface.py +26 -23
  55. chuk_tool_processor/registry/metadata.py +27 -22
  56. chuk_tool_processor/registry/provider.py +17 -18
  57. chuk_tool_processor/registry/providers/__init__.py +16 -19
  58. chuk_tool_processor/registry/providers/memory.py +18 -25
  59. chuk_tool_processor/registry/tool_export.py +42 -51
  60. chuk_tool_processor/utils/validation.py +15 -16
  61. chuk_tool_processor-0.9.7.dist-info/METADATA +1813 -0
  62. chuk_tool_processor-0.9.7.dist-info/RECORD +67 -0
  63. chuk_tool_processor-0.6.4.dist-info/METADATA +0 -697
  64. chuk_tool_processor-0.6.4.dist-info/RECORD +0 -60
  65. {chuk_tool_processor-0.6.4.dist-info → chuk_tool_processor-0.9.7.dist-info}/WHEEL +0 -0
  66. {chuk_tool_processor-0.6.4.dist-info → chuk_tool_processor-0.9.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1813 @@
1
+ Metadata-Version: 2.4
2
+ Name: chuk-tool-processor
3
+ Version: 0.9.7
4
+ Summary: Async-native framework for registering, discovering, and executing tools referenced in LLM responses
5
+ Author-email: CHUK Team <chrishayuk@somejunkmailbox.com>
6
+ Maintainer-email: CHUK Team <chrishayuk@somejunkmailbox.com>
7
+ License: MIT
8
+ Keywords: llm,tools,async,ai,openai,mcp,model-context-protocol,tool-calling,function-calling
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Classifier: Framework :: AsyncIO
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: chuk-mcp>=0.8.1
24
+ Requires-Dist: dotenv>=0.9.9
25
+ Requires-Dist: psutil>=7.0.0
26
+ Requires-Dist: pydantic>=2.11.3
27
+ Requires-Dist: uuid>=1.30
28
+
29
+ # CHUK Tool Processor
30
+
31
+ [![PyPI](https://img.shields.io/pypi/v/chuk-tool-processor.svg)](https://pypi.org/project/chuk-tool-processor/)
32
+ [![Python](https://img.shields.io/pypi/pyversions/chuk-tool-processor.svg)](https://pypi.org/project/chuk-tool-processor/)
33
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
34
+
35
+ **The missing link between LLM tool calls and reliable execution.**
36
+
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.
38
+
39
+ ## The Problem
40
+
41
+ When you build LLM applications, you face a gap:
42
+
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
53
+
54
+ Most frameworks give you steps 1 and 3, but step 2 is where the complexity lives. CHUK Tool Processor **is** step 2.
55
+
56
+ ## Why chuk-tool-processor?
57
+
58
+ ### It's a Building Block, Not a Framework
59
+
60
+ Unlike full-fledged LLM frameworks (LangChain, LlamaIndex, etc.), CHUK Tool Processor:
61
+
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
69
+
70
+ ### It's Built for Production
71
+
72
+ Research code vs production code is about handling the edges:
73
+
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)
84
+
85
+ ### It's About Stacks
86
+
87
+ CHUK Tool Processor uses a **composable stack architecture**:
88
+
89
+ ```
90
+ ┌─────────────────────────────────┐
91
+ │ Your LLM Application │
92
+ │ (handles prompts, responses) │
93
+ └────────────┬────────────────────┘
94
+ │ tool calls
95
+
96
+ ┌─────────────────────────────────┐
97
+ │ Caching Wrapper │ ← Cache expensive results (idempotency keys)
98
+ ├─────────────────────────────────┤
99
+ │ Rate Limiting Wrapper │ ← Prevent API abuse
100
+ ├─────────────────────────────────┤
101
+ │ Retry Wrapper │ ← Handle transient failures (exponential backoff)
102
+ ├─────────────────────────────────┤
103
+ │ Circuit Breaker Wrapper │ ← Prevent cascading failures (CLOSED/OPEN/HALF_OPEN)
104
+ ├─────────────────────────────────┤
105
+ │ Execution Strategy │ ← How to run tools
106
+ │ • InProcess (fast) │
107
+ │ • Subprocess (isolated) │
108
+ ├─────────────────────────────────┤
109
+ │ Tool Registry │ ← Your registered tools
110
+ └─────────────────────────────────┘
111
+ ```
112
+
113
+ Each layer is **optional** and **configurable**. Mix and match what you need.
114
+
115
+ ## Compatibility Matrix
116
+
117
+ | Component | Supported Versions | Notes |
118
+ |-----------|-------------------|-------|
119
+ | **Python** | 3.11, 3.12, 3.13 | Python 3.11+ required |
120
+ | **Operating Systems** | macOS, Linux, Windows | All platforms fully supported |
121
+ | **LLM Providers** | OpenAI, Anthropic, Local models | Any LLM that outputs tool calls |
122
+ | **MCP Transports** | HTTP Streamable, STDIO, SSE | All MCP 1.0 transports |
123
+ | **MCP Servers** | Notion, SQLite, Atlassian, Echo, Custom | Any MCP-compliant server |
124
+
125
+ **Tested Configurations:**
126
+ - ✅ macOS 14+ (Apple Silicon & Intel)
127
+ - ✅ Ubuntu 20.04+ / Debian 11+
128
+ - ✅ Windows 10+ (native & WSL2)
129
+ - ✅ Python 3.11.0+, 3.12.0+, 3.13.0+
130
+ - ✅ OpenAI GPT-4, GPT-4 Turbo
131
+ - ✅ Anthropic Claude 3 (Opus, Sonnet, Haiku)
132
+ - ✅ Local models (Ollama, LM Studio)
133
+
134
+ ## Quick Start
135
+
136
+ ### Installation
137
+
138
+ **Prerequisites:** Python 3.11+ • Works on macOS, Linux, Windows
139
+
140
+ ```bash
141
+ # Using pip
142
+ pip install chuk-tool-processor
143
+
144
+ # Using uv (recommended)
145
+ uv pip install chuk-tool-processor
146
+
147
+ # Or from source
148
+ git clone https://github.com/chrishayuk/chuk-tool-processor.git
149
+ cd chuk-tool-processor
150
+ uv pip install -e .
151
+ ```
152
+
153
+ ## 60-Second Quick Start
154
+
155
+ **Absolutely minimal example** → See `examples/hello_tool.py`:
156
+
157
+ ```bash
158
+ python examples/hello_tool.py
159
+ ```
160
+
161
+ Single file that demonstrates:
162
+ - Registering a tool
163
+ - Parsing OpenAI & Anthropic formats
164
+ - Executing and getting results
165
+
166
+ Takes 60 seconds to understand, 3 minutes to master.
167
+
168
+ ### 3-Minute Example
169
+
170
+ Copy-paste this into a file and run it:
171
+
172
+ ```python
173
+ import asyncio
174
+ from chuk_tool_processor.core.processor import ToolProcessor
175
+ from chuk_tool_processor.registry import initialize, register_tool
176
+
177
+ # Step 1: Define a tool
178
+ @register_tool(name="calculator")
179
+ class Calculator:
180
+ async def execute(self, operation: str, a: float, b: float) -> dict:
181
+ ops = {"add": a + b, "multiply": a * b, "subtract": a - b}
182
+ if operation not in ops:
183
+ raise ValueError(f"Unsupported operation: {operation}")
184
+ return {"result": ops[operation]}
185
+
186
+ # Step 2: Process LLM output
187
+ async def main():
188
+ await initialize()
189
+ processor = ToolProcessor()
190
+
191
+ # Your LLM returned this tool call
192
+ llm_output = '<tool name="calculator" args=\'{"operation": "multiply", "a": 15, "b": 23}\'/>'
193
+
194
+ # Process it
195
+ results = await processor.process(llm_output)
196
+
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}
204
+
205
+ asyncio.run(main())
206
+ ```
207
+
208
+ **That's it.** You now have production-ready tool execution with timeouts, retries, and caching.
209
+
210
+ > **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.
212
+
213
+ ## Documentation Quick Reference
214
+
215
+ | Document | What It Covers |
216
+ |----------|----------------|
217
+ | 📘 [CONFIGURATION.md](docs/CONFIGURATION.md) | **All config knobs & defaults**: ToolProcessor options, timeouts, retry policy, rate limits, circuit breakers, caching, environment variables |
218
+ | 🚨 [ERRORS.md](docs/ERRORS.md) | **Error taxonomy**: All error codes, exception classes, error details structure, handling patterns, retryability guide |
219
+ | 📊 [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 |
221
+ | 🎯 [examples/](examples/) | **20+ working examples**: MCP integration, OAuth flows, streaming, production patterns |
222
+
223
+ ## Choose Your Path
224
+
225
+ | Your Goal | What You Need | Where to Look |
226
+ |-----------|---------------|---------------|
227
+ | ☕ **Just process LLM tool calls** | Basic tool registration + processor | [60-Second Quick Start](#60-second-quick-start) |
228
+ | 🔌 **Connect to external tools** | MCP integration (HTTP/STDIO/SSE) | [MCP Integration](#5-mcp-integration-external-tools) |
229
+ | 🛡️ **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) |
231
+ | 📊 **Monitor and observe** | OpenTelemetry + Prometheus | [OBSERVABILITY.md](docs/OBSERVABILITY.md) |
232
+ | 🌊 **Stream incremental results** | StreamingTool pattern | [StreamingTool](#streamingtool-real-time-results) |
233
+ | 🚨 **Handle errors reliably** | Error codes & taxonomy | [ERRORS.md](docs/ERRORS.md) |
234
+
235
+ ### Real-World Quick Start
236
+
237
+ Here are the most common patterns you'll use:
238
+
239
+ **Pattern 1: Local tools only**
240
+ ```python
241
+ import asyncio
242
+ from chuk_tool_processor.core.processor import ToolProcessor
243
+ from chuk_tool_processor.registry import initialize, register_tool
244
+
245
+ @register_tool(name="my_tool")
246
+ class MyTool:
247
+ async def execute(self, arg: str) -> dict:
248
+ return {"result": f"Processed: {arg}"}
249
+
250
+ async def main():
251
+ await initialize()
252
+ processor = ToolProcessor()
253
+
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'}
257
+
258
+ asyncio.run(main())
259
+ ```
260
+
261
+ **Pattern 2: Mix local + remote MCP tools (Notion)**
262
+ ```python
263
+ import asyncio
264
+ from chuk_tool_processor.registry import initialize, register_tool
265
+ from chuk_tool_processor.mcp import setup_mcp_http_streamable
266
+
267
+ @register_tool(name="local_calculator")
268
+ class Calculator:
269
+ async def execute(self, a: int, b: int) -> int:
270
+ return a + b
271
+
272
+ async def main():
273
+ # Register local tools first
274
+ await initialize()
275
+
276
+ # Then add Notion MCP tools (requires OAuth token)
277
+ processor, manager = await setup_mcp_http_streamable(
278
+ servers=[{
279
+ "name": "notion",
280
+ "url": "https://mcp.notion.com/mcp",
281
+ "headers": {"Authorization": f"Bearer {access_token}"}
282
+ }],
283
+ namespace="notion",
284
+ initialization_timeout=120.0
285
+ )
286
+
287
+ # Now you have both local and remote tools!
288
+ results = await processor.process('''
289
+ <tool name="local_calculator" args='{"a": 5, "b": 3}'/>
290
+ <tool name="notion.search_pages" args='{"query": "project docs"}'/>
291
+ ''')
292
+ print(f"Local result: {results[0].result}")
293
+ print(f"Notion result: {results[1].result}")
294
+
295
+ asyncio.run(main())
296
+ ```
297
+
298
+ See `examples/notion_oauth.py` for complete OAuth flow.
299
+
300
+ **Pattern 3: Local SQLite database via STDIO**
301
+ ```python
302
+ import asyncio
303
+ import json
304
+ from chuk_tool_processor.mcp import setup_mcp_stdio
305
+
306
+ async def main():
307
+ # Configure SQLite MCP server (runs locally)
308
+ config = {
309
+ "mcpServers": {
310
+ "sqlite": {
311
+ "command": "uvx",
312
+ "args": ["mcp-server-sqlite", "--db-path", "./app.db"],
313
+ "transport": "stdio"
314
+ }
315
+ }
316
+ }
317
+
318
+ with open("mcp_config.json", "w") as f:
319
+ json.dump(config, f)
320
+
321
+ processor, manager = await setup_mcp_stdio(
322
+ config_file="mcp_config.json",
323
+ servers=["sqlite"],
324
+ namespace="db",
325
+ initialization_timeout=120.0 # First run downloads the package
326
+ )
327
+
328
+ # Query your local database via MCP
329
+ results = await processor.process(
330
+ '<tool name="db.query" args=\'{"sql": "SELECT * FROM users LIMIT 10"}\'/>'
331
+ )
332
+ print(results[0].result)
333
+
334
+ asyncio.run(main())
335
+ ```
336
+
337
+ See `examples/stdio_sqlite.py` for complete working example.
338
+
339
+ ## Core Concepts
340
+
341
+ ### 1. Tool Registry
342
+
343
+ The **registry** is where you register tools for execution. Tools can be:
344
+
345
+ - **Simple classes** with an `async execute()` method
346
+ - **ValidatedTool** subclasses with Pydantic validation
347
+ - **StreamingTool** for real-time incremental results
348
+ - **Functions** registered via `register_fn_tool()`
349
+
350
+ ```python
351
+ from chuk_tool_processor.registry import register_tool
352
+ from chuk_tool_processor.models.validated_tool import ValidatedTool
353
+ from pydantic import BaseModel, Field
354
+
355
+ @register_tool(name="weather")
356
+ class WeatherTool(ValidatedTool):
357
+ class Arguments(BaseModel):
358
+ location: str = Field(..., description="City name")
359
+ units: str = Field("celsius", description="Temperature units")
360
+
361
+ class Result(BaseModel):
362
+ temperature: float
363
+ conditions: str
364
+
365
+ async def _execute(self, location: str, units: str) -> Result:
366
+ # Your weather API logic here
367
+ return self.Result(temperature=22.5, conditions="Sunny")
368
+ ```
369
+
370
+ ### 2. Execution Strategies
371
+
372
+ **Strategies** determine *how* tools run:
373
+
374
+ | Strategy | Use Case | Trade-offs |
375
+ |----------|----------|------------|
376
+ | **InProcessStrategy** | Fast, trusted tools | Speed ✅, Isolation ❌ |
377
+ | **SubprocessStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
378
+
379
+ ```python
380
+ 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
384
+
385
+ async def main():
386
+ registry = await get_default_registry()
387
+ processor = ToolProcessor(
388
+ strategy=SubprocessStrategy(
389
+ registry=registry,
390
+ max_workers=4,
391
+ default_timeout=30.0
392
+ )
393
+ )
394
+ # Use processor...
395
+
396
+ asyncio.run(main())
397
+ ```
398
+
399
+ ### 3. Execution Wrappers (Middleware)
400
+
401
+ **Wrappers** add production features as composable layers:
402
+
403
+ ```python
404
+ processor = ToolProcessor(
405
+ enable_caching=True, # Cache expensive calls
406
+ cache_ttl=600, # 10 minutes
407
+ enable_rate_limiting=True, # Prevent abuse
408
+ global_rate_limit=100, # 100 req/min globally
409
+ enable_retries=True, # Auto-retry failures
410
+ max_retries=3 # Up to 3 attempts
411
+ )
412
+ ```
413
+
414
+ The processor stacks them automatically: **Cache → Rate Limit → Retry → Strategy → Tool**
415
+
416
+ ### 4. Input Parsers (Plugins)
417
+
418
+ **Parsers** extract tool calls from various LLM output formats:
419
+
420
+ **XML Tags (Anthropic-style)**
421
+ ```xml
422
+ <tool name="search" args='{"query": "Python"}'/>
423
+ ```
424
+
425
+ **OpenAI `tool_calls` (JSON)**
426
+ ```json
427
+ {
428
+ "tool_calls": [
429
+ {
430
+ "type": "function",
431
+ "function": {
432
+ "name": "search",
433
+ "arguments": "{\"query\": \"Python\"}"
434
+ }
435
+ }
436
+ ]
437
+ }
438
+ ```
439
+
440
+ **Direct JSON (array of calls)**
441
+ ```json
442
+ [
443
+ { "tool": "search", "arguments": { "query": "Python" } }
444
+ ]
445
+ ```
446
+
447
+ All formats work automatically—no configuration needed.
448
+
449
+ **Input Format Compatibility:**
450
+
451
+ | Format | Example | Use Case |
452
+ |--------|---------|----------|
453
+ | **XML Tool Tag** | `<tool name="search" args='{"q":"Python"}'/>`| Anthropic Claude, XML-based LLMs |
454
+ | **OpenAI tool_calls** | JSON object (above) | OpenAI GPT-4 function calling |
455
+ | **Direct JSON** | `[{"tool": "search", "arguments": {"q": "Python"}}]` | Generic API integrations |
456
+ | **Single dict** | `{"tool": "search", "arguments": {"q": "Python"}}` | Programmatic calls |
457
+
458
+ ### 5. MCP Integration (External Tools)
459
+
460
+ Connect to **remote tool servers** using the [Model Context Protocol](https://modelcontextprotocol.io). CHUK Tool Processor supports three transport mechanisms for different use cases:
461
+
462
+ #### HTTP Streamable (⭐ Recommended for Cloud Services)
463
+
464
+ Modern HTTP streaming transport for cloud-based MCP servers like Notion:
465
+
466
+ ```python
467
+ from chuk_tool_processor.mcp import setup_mcp_http_streamable
468
+
469
+ # Connect to Notion MCP with OAuth
470
+ servers = [
471
+ {
472
+ "name": "notion",
473
+ "url": "https://mcp.notion.com/mcp",
474
+ "headers": {"Authorization": f"Bearer {access_token}"}
475
+ }
476
+ ]
477
+
478
+ processor, manager = await setup_mcp_http_streamable(
479
+ servers=servers,
480
+ namespace="notion",
481
+ initialization_timeout=120.0, # Some services need time to initialize
482
+ enable_caching=True,
483
+ enable_retries=True
484
+ )
485
+
486
+ # Use Notion tools through MCP
487
+ results = await processor.process(
488
+ '<tool name="notion.search_pages" args=\'{"query": "meeting notes"}\'/>'
489
+ )
490
+ ```
491
+
492
+ #### STDIO (Best for Local/On-Device Tools)
493
+
494
+ For running local MCP servers as subprocesses—great for databases, file systems, and local tools:
495
+
496
+ ```python
497
+ from chuk_tool_processor.mcp import setup_mcp_stdio
498
+ import json
499
+
500
+ # Configure SQLite MCP server
501
+ config = {
502
+ "mcpServers": {
503
+ "sqlite": {
504
+ "command": "uvx",
505
+ "args": ["mcp-server-sqlite", "--db-path", "/path/to/database.db"],
506
+ "env": {"MCP_SERVER_NAME": "sqlite"},
507
+ "transport": "stdio"
508
+ }
509
+ }
510
+ }
511
+
512
+ # Save config to file
513
+ with open("mcp_config.json", "w") as f:
514
+ json.dump(config, f)
515
+
516
+ # Connect to local SQLite server
517
+ processor, manager = await setup_mcp_stdio(
518
+ config_file="mcp_config.json",
519
+ servers=["sqlite"],
520
+ namespace="db",
521
+ initialization_timeout=120.0 # First run downloads packages
522
+ )
523
+
524
+ # Query your local database via MCP
525
+ results = await processor.process(
526
+ '<tool name="db.query" args=\'{"sql": "SELECT * FROM users LIMIT 10"}\'/>'
527
+ )
528
+ ```
529
+
530
+ #### SSE (Legacy Support)
531
+
532
+ For backward compatibility with older MCP servers using Server-Sent Events:
533
+
534
+ ```python
535
+ from chuk_tool_processor.mcp import setup_mcp_sse
536
+
537
+ # Connect to Atlassian with OAuth via SSE
538
+ servers = [
539
+ {
540
+ "name": "atlassian",
541
+ "url": "https://mcp.atlassian.com/v1/sse",
542
+ "headers": {"Authorization": f"Bearer {access_token}"}
543
+ }
544
+ ]
545
+
546
+ processor, manager = await setup_mcp_sse(
547
+ servers=servers,
548
+ namespace="atlassian",
549
+ initialization_timeout=120.0
550
+ )
551
+ ```
552
+
553
+ **Transport Comparison:**
554
+
555
+ | Transport | Use Case | Real Examples |
556
+ |-----------|----------|---------------|
557
+ | **HTTP Streamable** | Cloud APIs, SaaS services | Notion (`mcp.notion.com`) |
558
+ | **STDIO** | Local tools, databases | SQLite (`mcp-server-sqlite`), Echo (`chuk-mcp-echo`) |
559
+ | **SSE** | Legacy cloud services | Atlassian (`mcp.atlassian.com`) |
560
+
561
+ **Relationship with [chuk-mcp](https://github.com/chrishayuk/chuk-mcp):**
562
+ - `chuk-mcp` is a low-level MCP protocol client (handles transports, protocol negotiation)
563
+ - `chuk-tool-processor` wraps `chuk-mcp` to integrate external tools into your execution pipeline
564
+ - You can use local tools, remote MCP tools, or both in the same processor
565
+
566
+ ## Getting Started
567
+
568
+ ### Creating Tools
569
+
570
+ CHUK Tool Processor supports multiple patterns for defining tools:
571
+
572
+ #### Simple Function-Based Tools
573
+ ```python
574
+ from chuk_tool_processor.registry.auto_register import register_fn_tool
575
+ from datetime import datetime
576
+ from zoneinfo import ZoneInfo
577
+
578
+ def get_current_time(timezone: str = "UTC") -> str:
579
+ """Get the current time in the specified timezone."""
580
+ now = datetime.now(ZoneInfo(timezone))
581
+ return now.strftime("%Y-%m-%d %H:%M:%S %Z")
582
+
583
+ # Register the function as a tool (sync — no await needed)
584
+ register_fn_tool(get_current_time, namespace="utilities")
585
+ ```
586
+
587
+ #### ValidatedTool (Pydantic Type Safety)
588
+
589
+ For production tools, use Pydantic validation:
590
+
591
+ ```python
592
+ @register_tool(name="weather")
593
+ class WeatherTool(ValidatedTool):
594
+ class Arguments(BaseModel):
595
+ location: str = Field(..., description="City name")
596
+ units: str = Field("celsius", description="Temperature units")
597
+
598
+ class Result(BaseModel):
599
+ temperature: float
600
+ conditions: str
601
+
602
+ async def _execute(self, location: str, units: str) -> Result:
603
+ return self.Result(temperature=22.5, conditions="Sunny")
604
+ ```
605
+
606
+ #### StreamingTool (Real-time Results)
607
+
608
+ For long-running operations that produce incremental results:
609
+
610
+ ```python
611
+ from chuk_tool_processor.models import StreamingTool
612
+
613
+ @register_tool(name="file_processor")
614
+ class FileProcessor(StreamingTool):
615
+ class Arguments(BaseModel):
616
+ file_path: str
617
+
618
+ class Result(BaseModel):
619
+ line: int
620
+ content: str
621
+
622
+ async def _stream_execute(self, file_path: str):
623
+ with open(file_path) as f:
624
+ for i, line in enumerate(f, 1):
625
+ yield self.Result(line=i, content=line.strip())
626
+ ```
627
+
628
+ **Consuming streaming results:**
629
+
630
+ ```python
631
+ import asyncio
632
+ from chuk_tool_processor.core.processor import ToolProcessor
633
+ from chuk_tool_processor.registry import initialize
634
+
635
+ async def main():
636
+ await initialize()
637
+ 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}")
643
+
644
+ asyncio.run(main())
645
+ ```
646
+
647
+ ### Using the Processor
648
+
649
+ #### Basic Usage
650
+
651
+ Call `await initialize()` once at startup to load your registry.
652
+
653
+ ```python
654
+ import asyncio
655
+ from chuk_tool_processor.core.processor import ToolProcessor
656
+ from chuk_tool_processor.registry import initialize
657
+
658
+ async def main():
659
+ 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}")
668
+
669
+ asyncio.run(main())
670
+ ```
671
+
672
+ #### Production Configuration
673
+
674
+ ```python
675
+ from chuk_tool_processor.core.processor import ToolProcessor
676
+
677
+ processor = ToolProcessor(
678
+ # Execution settings
679
+ default_timeout=30.0,
680
+ max_concurrency=20,
681
+
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
+ )
690
+ ```
691
+
692
+ ### Advanced Production Features
693
+
694
+ Beyond basic configuration, CHUK Tool Processor includes several advanced features for production environments:
695
+
696
+ #### Circuit Breaker Pattern
697
+
698
+ Prevent cascading failures by automatically opening circuits for failing tools:
699
+
700
+ ```python
701
+ from chuk_tool_processor.core.processor import ToolProcessor
702
+
703
+ processor = ToolProcessor(
704
+ enable_circuit_breaker=True,
705
+ circuit_breaker_threshold=5, # Open after 5 failures
706
+ circuit_breaker_timeout=60.0, # Try recovery after 60s
707
+ )
708
+
709
+ # Circuit states: CLOSED → OPEN → HALF_OPEN → CLOSED
710
+ # - CLOSED: Normal operation
711
+ # - OPEN: Blocking requests (too many failures)
712
+ # - HALF_OPEN: Testing recovery with limited requests
713
+ ```
714
+
715
+ **How it works:**
716
+ 1. Tool fails repeatedly (hits threshold)
717
+ 2. Circuit opens → requests blocked immediately
718
+ 3. After timeout, circuit enters HALF_OPEN
719
+ 4. If test requests succeed → circuit closes
720
+ 5. If test requests fail → back to OPEN
721
+
722
+ **Benefits:**
723
+ - Prevents wasting resources on failing services
724
+ - Fast-fail for better UX
725
+ - Automatic recovery detection
726
+
727
+ #### Idempotency Keys
728
+
729
+ Automatically deduplicate LLM tool calls using SHA256-based keys:
730
+
731
+ ```python
732
+ from chuk_tool_processor.models.tool_call import ToolCall
733
+
734
+ # Idempotency keys are auto-generated
735
+ call1 = ToolCall(tool="search", arguments={"query": "Python"})
736
+ call2 = ToolCall(tool="search", arguments={"query": "Python"})
737
+
738
+ # Same arguments = same idempotency key
739
+ assert call1.idempotency_key == call2.idempotency_key
740
+
741
+ # Used automatically by caching layer
742
+ processor = ToolProcessor(enable_caching=True)
743
+ results1 = await processor.execute([call1]) # Executes
744
+ results2 = await processor.execute([call2]) # Cache hit!
745
+ ```
746
+
747
+ **Benefits:**
748
+ - Prevents duplicate executions from LLM retries
749
+ - Deterministic cache keys
750
+ - No manual key management needed
751
+
752
+ #### Tool Schema Export
753
+
754
+ Export tool definitions to multiple formats for LLM prompting:
755
+
756
+ ```python
757
+ from chuk_tool_processor.models.tool_spec import ToolSpec, ToolCapability
758
+ from chuk_tool_processor.models.validated_tool import ValidatedTool
759
+
760
+ @register_tool(name="weather")
761
+ class WeatherTool(ValidatedTool):
762
+ """Get current weather for a location."""
763
+
764
+ class Arguments(BaseModel):
765
+ location: str = Field(..., description="City name")
766
+
767
+ class Result(BaseModel):
768
+ temperature: float
769
+ conditions: str
770
+
771
+ # Generate tool spec
772
+ spec = ToolSpec.from_validated_tool(WeatherTool)
773
+
774
+ # Export to different formats
775
+ openai_format = spec.to_openai() # For OpenAI function calling
776
+ anthropic_format = spec.to_anthropic() # For Claude tools
777
+ mcp_format = spec.to_mcp() # For MCP servers
778
+
779
+ # Example OpenAI format:
780
+ # {
781
+ # "type": "function",
782
+ # "function": {
783
+ # "name": "weather",
784
+ # "description": "Get current weather for a location.",
785
+ # "parameters": {...} # JSON Schema
786
+ # }
787
+ # }
788
+ ```
789
+
790
+ **Use cases:**
791
+ - Generate tool definitions for LLM system prompts
792
+ - Documentation generation
793
+ - API contract validation
794
+ - Cross-platform tool sharing
795
+
796
+ #### Machine-Readable Error Codes
797
+
798
+ Structured error handling with error codes for programmatic responses:
799
+
800
+ ```python
801
+ from chuk_tool_processor.core.exceptions import (
802
+ ErrorCode,
803
+ ToolNotFoundError,
804
+ ToolTimeoutError,
805
+ ToolCircuitOpenError,
806
+ )
807
+
808
+ try:
809
+ results = await processor.process(llm_output)
810
+ except ToolNotFoundError as e:
811
+ if e.code == ErrorCode.TOOL_NOT_FOUND:
812
+ # Suggest available tools to LLM
813
+ available = e.details.get("available_tools", [])
814
+ print(f"Try one of: {available}")
815
+ except ToolTimeoutError as e:
816
+ if e.code == ErrorCode.TOOL_TIMEOUT:
817
+ # Inform LLM to use faster alternative
818
+ timeout = e.details["timeout"]
819
+ print(f"Tool timed out after {timeout}s")
820
+ except ToolCircuitOpenError as e:
821
+ if e.code == ErrorCode.TOOL_CIRCUIT_OPEN:
822
+ # Tell LLM this service is temporarily down
823
+ reset_time = e.details.get("reset_timeout")
824
+ print(f"Service unavailable, retry in {reset_time}s")
825
+
826
+ # All errors include .to_dict() for logging
827
+ error_dict = e.to_dict()
828
+ # {
829
+ # "error": "ToolCircuitOpenError",
830
+ # "code": "TOOL_CIRCUIT_OPEN",
831
+ # "message": "Tool 'api_tool' circuit breaker is open...",
832
+ # "details": {"tool_name": "api_tool", "failure_count": 5, ...}
833
+ # }
834
+ ```
835
+
836
+ **Available error codes:**
837
+ - `TOOL_NOT_FOUND` - Tool doesn't exist in registry
838
+ - `TOOL_EXECUTION_FAILED` - Tool execution error
839
+ - `TOOL_TIMEOUT` - Tool exceeded timeout
840
+ - `TOOL_CIRCUIT_OPEN` - Circuit breaker is open
841
+ - `TOOL_RATE_LIMITED` - Rate limit exceeded
842
+ - `TOOL_VALIDATION_ERROR` - Argument validation failed
843
+ - `MCP_CONNECTION_FAILED` - MCP server unreachable
844
+ - Plus 11 more for comprehensive error handling
845
+
846
+ #### LLM-Friendly Argument Coercion
847
+
848
+ Automatically coerce LLM outputs to correct types:
849
+
850
+ ```python
851
+ from chuk_tool_processor.models.validated_tool import ValidatedTool
852
+
853
+ class SearchTool(ValidatedTool):
854
+ class Arguments(BaseModel):
855
+ query: str
856
+ limit: int = 10
857
+ category: str = "all"
858
+
859
+ # Pydantic config for LLM outputs:
860
+ # - str_strip_whitespace=True → Remove accidental whitespace
861
+ # - extra="ignore" → Ignore unknown fields
862
+ # - use_enum_values=True → Convert enums to values
863
+ # - coerce_numbers_to_str=False → Keep type strictness
864
+
865
+ # LLM outputs often have quirks:
866
+ llm_output = {
867
+ "query": " Python tutorials ", # Extra whitespace
868
+ "limit": "5", # String instead of int
869
+ "unknown_field": "ignored" # Extra field
870
+ }
871
+
872
+ # ValidatedTool automatically coerces and validates
873
+ tool = SearchTool()
874
+ result = await tool.execute(**llm_output)
875
+ # ✅ Works! Whitespace stripped, "5" → 5, extra field ignored
876
+ ```
877
+
878
+ ## Advanced Topics
879
+
880
+ ### Using Subprocess Strategy
881
+
882
+ Use `SubprocessStrategy` when running untrusted, third-party, or potentially unsafe code that shouldn't share the same process as your main app.
883
+
884
+ For isolation and safety when running untrusted code:
885
+
886
+ ```python
887
+ 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
891
+
892
+ async def main():
893
+ registry = await get_default_registry()
894
+ processor = ToolProcessor(
895
+ strategy=SubprocessStrategy(
896
+ registry=registry,
897
+ max_workers=4,
898
+ default_timeout=30.0
899
+ )
900
+ )
901
+ # Use processor...
902
+
903
+ asyncio.run(main())
904
+ ```
905
+
906
+ ### Real-World MCP Examples
907
+
908
+ #### Example 1: Notion Integration with OAuth
909
+
910
+ Complete OAuth flow connecting to Notion's MCP server:
911
+
912
+ ```python
913
+ from chuk_tool_processor.mcp import setup_mcp_http_streamable
914
+
915
+ # After completing OAuth flow (see examples/notion_oauth.py for full flow)
916
+ processor, manager = await setup_mcp_http_streamable(
917
+ servers=[{
918
+ "name": "notion",
919
+ "url": "https://mcp.notion.com/mcp",
920
+ "headers": {"Authorization": f"Bearer {access_token}"}
921
+ }],
922
+ namespace="notion",
923
+ initialization_timeout=120.0
924
+ )
925
+
926
+ # Get available Notion tools
927
+ tools = manager.get_all_tools()
928
+ print(f"Available tools: {[t['name'] for t in tools]}")
929
+
930
+ # Use Notion tools in your LLM workflow
931
+ results = await processor.process(
932
+ '<tool name="notion.search_pages" args=\'{"query": "Q4 planning"}\'/>'
933
+ )
934
+ ```
935
+
936
+ #### Example 2: Local SQLite Database Access
937
+
938
+ Run SQLite MCP server locally for database operations:
939
+
940
+ ```python
941
+ from chuk_tool_processor.mcp import setup_mcp_stdio
942
+ import json
943
+
944
+ # Configure SQLite server
945
+ config = {
946
+ "mcpServers": {
947
+ "sqlite": {
948
+ "command": "uvx",
949
+ "args": ["mcp-server-sqlite", "--db-path", "./data/app.db"],
950
+ "transport": "stdio"
951
+ }
952
+ }
953
+ }
954
+
955
+ with open("mcp_config.json", "w") as f:
956
+ json.dump(config, f)
957
+
958
+ # Connect to local database
959
+ processor, manager = await setup_mcp_stdio(
960
+ config_file="mcp_config.json",
961
+ servers=["sqlite"],
962
+ namespace="db",
963
+ initialization_timeout=120.0 # First run downloads mcp-server-sqlite
964
+ )
965
+
966
+ # Query your database via LLM
967
+ results = await processor.process(
968
+ '<tool name="db.query" args=\'{"sql": "SELECT COUNT(*) FROM users"}\'/>'
969
+ )
970
+ ```
971
+
972
+ #### Example 3: Simple STDIO Echo Server
973
+
974
+ Minimal example for testing STDIO transport:
975
+
976
+ ```python
977
+ from chuk_tool_processor.mcp import setup_mcp_stdio
978
+ import json
979
+
980
+ # Configure echo server (great for testing)
981
+ config = {
982
+ "mcpServers": {
983
+ "echo": {
984
+ "command": "uvx",
985
+ "args": ["chuk-mcp-echo", "stdio"],
986
+ "transport": "stdio"
987
+ }
988
+ }
989
+ }
990
+
991
+ with open("echo_config.json", "w") as f:
992
+ json.dump(config, f)
993
+
994
+ processor, manager = await setup_mcp_stdio(
995
+ config_file="echo_config.json",
996
+ servers=["echo"],
997
+ namespace="echo",
998
+ initialization_timeout=60.0
999
+ )
1000
+
1001
+ # Test echo functionality
1002
+ results = await processor.process(
1003
+ '<tool name="echo.echo" args=\'{"message": "Hello MCP!"}\'/>'
1004
+ )
1005
+ ```
1006
+
1007
+ See `examples/notion_oauth.py`, `examples/stdio_sqlite.py`, and `examples/stdio_echo.py` for complete working implementations.
1008
+
1009
+ #### OAuth Token Refresh
1010
+
1011
+ 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
+
1013
+ **How it works:**
1014
+ 1. When a tool call receives an OAuth-related error (e.g., "invalid_token", "expired token", "unauthorized")
1015
+ 2. The processor automatically calls your refresh callback
1016
+ 3. Updates the authentication headers with the new token
1017
+ 4. Retries the tool call with fresh credentials
1018
+
1019
+ **Setup with HTTP Streamable:**
1020
+
1021
+ ```python
1022
+ from chuk_tool_processor.mcp import setup_mcp_http_streamable
1023
+
1024
+ async def refresh_oauth_token():
1025
+ """Called automatically when tokens expire."""
1026
+ # Your token refresh logic here
1027
+ # Return dict with new Authorization header
1028
+ new_token = await your_refresh_logic()
1029
+ return {"Authorization": f"Bearer {new_token}"}
1030
+
1031
+ processor, manager = await setup_mcp_http_streamable(
1032
+ servers=[{
1033
+ "name": "notion",
1034
+ "url": "https://mcp.notion.com/mcp",
1035
+ "headers": {"Authorization": f"Bearer {initial_access_token}"}
1036
+ }],
1037
+ namespace="notion",
1038
+ oauth_refresh_callback=refresh_oauth_token # Enable auto-refresh
1039
+ )
1040
+ ```
1041
+
1042
+ **Setup with SSE:**
1043
+
1044
+ ```python
1045
+ from chuk_tool_processor.mcp import setup_mcp_sse
1046
+
1047
+ async def refresh_oauth_token():
1048
+ """Refresh expired OAuth token."""
1049
+ # Exchange refresh token for new access token
1050
+ new_access_token = await exchange_refresh_token(refresh_token)
1051
+ return {"Authorization": f"Bearer {new_access_token}"}
1052
+
1053
+ processor, manager = await setup_mcp_sse(
1054
+ servers=[{
1055
+ "name": "atlassian",
1056
+ "url": "https://mcp.atlassian.com/v1/sse",
1057
+ "headers": {"Authorization": f"Bearer {initial_token}"}
1058
+ }],
1059
+ namespace="atlassian",
1060
+ oauth_refresh_callback=refresh_oauth_token
1061
+ )
1062
+ ```
1063
+
1064
+ **OAuth errors detected automatically:**
1065
+ - `invalid_token`
1066
+ - `expired token`
1067
+ - `OAuth validation failed`
1068
+ - `unauthorized`
1069
+ - `token expired`
1070
+ - `authentication failed`
1071
+ - `invalid access token`
1072
+
1073
+ **Important notes:**
1074
+ - The refresh callback must return a dict with an `Authorization` key
1075
+ - If refresh fails or returns invalid headers, the original error is returned
1076
+ - Token refresh is attempted only once per tool call (no infinite retry loops)
1077
+ - After successful refresh, the updated headers are used for all subsequent calls
1078
+
1079
+ See `examples/notion_oauth.py` for a complete OAuth 2.1 implementation with PKCE and automatic token refresh.
1080
+
1081
+ ### Observability
1082
+
1083
+ #### Structured Logging
1084
+
1085
+ Enable JSON logging for production observability:
1086
+
1087
+ ```python
1088
+ import asyncio
1089
+ from chuk_tool_processor.logging import setup_logging, get_logger
1090
+
1091
+ async def main():
1092
+ await setup_logging(
1093
+ level="INFO",
1094
+ structured=True, # JSON output (structured=False for human-readable)
1095
+ log_file="tool_processor.log"
1096
+ )
1097
+ logger = get_logger("my_app")
1098
+ logger.info("logging ready")
1099
+
1100
+ asyncio.run(main())
1101
+ ```
1102
+
1103
+ When `structured=True`, logs are output as JSON. When `structured=False`, they're human-readable text.
1104
+
1105
+ Example JSON log output:
1106
+
1107
+ ```json
1108
+ {
1109
+ "timestamp": "2025-01-15T10:30:45.123Z",
1110
+ "level": "INFO",
1111
+ "tool": "calculator",
1112
+ "status": "success",
1113
+ "duration_ms": 4.2,
1114
+ "cached": false,
1115
+ "attempts": 1
1116
+ }
1117
+ ```
1118
+
1119
+ #### Automatic Metrics
1120
+
1121
+ Metrics are automatically collected for:
1122
+ - ✅ Tool execution (success/failure rates, duration)
1123
+ - ✅ Cache performance (hit/miss rates)
1124
+ - ✅ Parser accuracy (which parsers succeeded)
1125
+ - ✅ Retry attempts (how many retries per tool)
1126
+
1127
+ Access metrics programmatically:
1128
+
1129
+ ```python
1130
+ import asyncio
1131
+ from chuk_tool_processor.logging import metrics
1132
+
1133
+ async def main():
1134
+ # Metrics are logged automatically, but you can also access them
1135
+ await metrics.log_tool_execution(
1136
+ tool="custom_tool",
1137
+ success=True,
1138
+ duration=1.5,
1139
+ cached=False,
1140
+ attempts=1
1141
+ )
1142
+
1143
+ asyncio.run(main())
1144
+ ```
1145
+
1146
+ #### OpenTelemetry & Prometheus (Drop-in Observability)
1147
+
1148
+ **3-Line Setup:**
1149
+
1150
+ ```python
1151
+ from chuk_tool_processor.observability import setup_observability
1152
+
1153
+ setup_observability(
1154
+ service_name="my-tool-service",
1155
+ enable_tracing=True, # → OpenTelemetry traces
1156
+ enable_metrics=True, # → Prometheus metrics at :9090/metrics
1157
+ metrics_port=9090
1158
+ )
1159
+ # That's it! Every tool execution is now automatically traced and metered.
1160
+ ```
1161
+
1162
+ **What you get automatically:**
1163
+ - ✅ Distributed traces (Jaeger, Zipkin, any OTLP collector)
1164
+ - ✅ Prometheus metrics (error rate, latency P50/P95/P99, cache hit rate)
1165
+ - ✅ Circuit breaker state monitoring
1166
+ - ✅ Retry attempt tracking
1167
+ - ✅ Zero code changes to your tools
1168
+
1169
+ **Why Telemetry Matters**: In production, you need to know *what* your tools are doing, *how long* they take, *when* they fail, and *why*. CHUK Tool Processor provides **enterprise-grade telemetry** that operations teams expect—with zero manual instrumentation.
1170
+
1171
+ **What You Get (Automatically)**
1172
+
1173
+ ✅ **Distributed Traces** - Understand exactly what happened in each tool call
1174
+ - See the complete execution timeline for every tool
1175
+ - Track retries, cache hits, circuit breaker state changes
1176
+ - Correlate failures across your system
1177
+ - Export to Jaeger, Zipkin, or any OTLP-compatible backend
1178
+
1179
+ ✅ **Production Metrics** - Monitor health and performance in real-time
1180
+ - Track error rates, latency percentiles (P50/P95/P99)
1181
+ - Monitor cache hit rates and retry attempts
1182
+ - Alert on circuit breaker opens and rate limit hits
1183
+ - Export to Prometheus, Grafana, or any metrics backend
1184
+
1185
+ ✅ **Zero Configuration** - Works out of the box
1186
+ - No manual instrumentation needed
1187
+ - No code changes to existing tools
1188
+ - Gracefully degrades if packages not installed
1189
+ - Standard OTEL and Prometheus formats
1190
+
1191
+ **Installation**
1192
+
1193
+ ```bash
1194
+ # Install observability dependencies
1195
+ pip install chuk-tool-processor[observability]
1196
+
1197
+ # Or manually
1198
+ pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp prometheus-client
1199
+
1200
+ # Or with uv (recommended)
1201
+ uv pip install chuk-tool-processor --group observability
1202
+ ```
1203
+
1204
+ > **⚠️ SRE Note**: Observability packages are **optional**. If not installed, all observability calls are no-ops—your tools run normally without tracing/metrics. Zero crashes, zero warnings. Safe to deploy without observability dependencies.
1205
+
1206
+ **Quick Start: See Your Tools in Action**
1207
+
1208
+ ```python
1209
+ import asyncio
1210
+ 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
1213
+
1214
+ @register_tool(name="weather_api")
1215
+ class WeatherTool:
1216
+ async def execute(self, location: str) -> dict:
1217
+ # Simulating API call
1218
+ return {"temperature": 72, "conditions": "sunny", "location": location}
1219
+
1220
+ async def main():
1221
+ # 1. Enable observability (one line!)
1222
+ setup_observability(
1223
+ service_name="weather-service",
1224
+ enable_tracing=True,
1225
+ enable_metrics=True,
1226
+ metrics_port=9090
1227
+ )
1228
+
1229
+ # 2. Create processor with production features
1230
+ await initialize()
1231
+ processor = ToolProcessor(
1232
+ enable_caching=True, # Cache expensive API calls
1233
+ enable_retries=True, # Auto-retry on failures
1234
+ enable_circuit_breaker=True, # Prevent cascading failures
1235
+ enable_rate_limiting=True, # Prevent API abuse
1236
+ )
1237
+
1238
+ # 3. Execute tools - automatically traced and metered
1239
+ results = await processor.process(
1240
+ '<tool name="weather_api" args=\'{"location": "San Francisco"}\'/>'
1241
+ )
1242
+
1243
+ print(f"Result: {results[0].result}")
1244
+ print(f"Duration: {results[0].duration}s")
1245
+ print(f"Cached: {results[0].cached}")
1246
+
1247
+ asyncio.run(main())
1248
+ ```
1249
+
1250
+ **View Your Data**
1251
+
1252
+ ```bash
1253
+ # Start Jaeger for trace visualization
1254
+ docker run -d -p 4317:4317 -p 16686:16686 jaegertracing/all-in-one:latest
1255
+
1256
+ # Start your application
1257
+ python your_app.py
1258
+
1259
+ # View distributed traces
1260
+ open http://localhost:16686
1261
+
1262
+ # View Prometheus metrics
1263
+ curl http://localhost:9090/metrics | grep tool_
1264
+ ```
1265
+
1266
+ **What Gets Traced (Automatic Spans)**
1267
+
1268
+ Every execution layer creates standardized OpenTelemetry spans:
1269
+
1270
+ | Span Name | When Created | Key Attributes |
1271
+ |-----------|--------------|----------------|
1272
+ | `tool.execute` | Every tool execution | `tool.name`, `tool.namespace`, `tool.duration_ms`, `tool.cached`, `tool.error`, `tool.success` |
1273
+ | `tool.cache.lookup` | Cache lookup | `cache.hit` (true/false), `cache.operation=lookup` |
1274
+ | `tool.cache.set` | Cache write | `cache.ttl`, `cache.operation=set` |
1275
+ | `tool.retry.attempt` | Each retry | `retry.attempt`, `retry.max_attempts`, `retry.success` |
1276
+ | `tool.circuit_breaker.check` | Circuit state check | `circuit.state` (CLOSED/OPEN/HALF_OPEN) |
1277
+ | `tool.rate_limit.check` | Rate limit check | `rate_limit.allowed` (true/false) |
1278
+
1279
+ **Example trace hierarchy:**
1280
+ ```
1281
+ tool.execute (weather_api)
1282
+ ├── tool.cache.lookup (miss)
1283
+ ├── tool.retry.attempt (0)
1284
+ │ └── tool.execute (actual API call)
1285
+ ├── tool.retry.attempt (1) [if first failed]
1286
+ └── tool.cache.set (store result)
1287
+ ```
1288
+
1289
+ **What Gets Metered (Automatic Metrics)**
1290
+
1291
+ Standard Prometheus metrics exposed at `/metrics`:
1292
+
1293
+ | Metric | Type | Labels | Use For |
1294
+ |--------|------|--------|---------|
1295
+ | `tool_executions_total` | Counter | `tool`, `namespace`, `status` | Error rate, request volume |
1296
+ | `tool_execution_duration_seconds` | Histogram | `tool`, `namespace` | P50/P95/P99 latency |
1297
+ | `tool_cache_operations_total` | Counter | `tool`, `operation`, `result` | Cache hit rate |
1298
+ | `tool_retry_attempts_total` | Counter | `tool`, `attempt`, `success` | Retry frequency |
1299
+ | `tool_circuit_breaker_state` | Gauge | `tool` | Circuit health (0=CLOSED, 1=OPEN, 2=HALF_OPEN) |
1300
+ | `tool_circuit_breaker_failures_total` | Counter | `tool` | Failure count |
1301
+ | `tool_rate_limit_checks_total` | Counter | `tool`, `allowed` | Rate limit hits |
1302
+
1303
+ **Useful PromQL Queries**
1304
+
1305
+ ```promql
1306
+ # Error rate per tool (last 5 minutes)
1307
+ rate(tool_executions_total{status="error"}[5m])
1308
+ / rate(tool_executions_total[5m])
1309
+
1310
+ # P95 latency
1311
+ histogram_quantile(0.95, rate(tool_execution_duration_seconds_bucket[5m]))
1312
+
1313
+ # Cache hit rate
1314
+ rate(tool_cache_operations_total{result="hit"}[5m])
1315
+ / rate(tool_cache_operations_total{operation="lookup"}[5m])
1316
+
1317
+ # Tools currently circuit broken
1318
+ tool_circuit_breaker_state == 1
1319
+
1320
+ # Retry rate (how often tools need retries)
1321
+ rate(tool_retry_attempts_total{attempt!="0"}[5m])
1322
+ / rate(tool_executions_total[5m])
1323
+ ```
1324
+
1325
+ **Configuration**
1326
+
1327
+ Configure via environment variables:
1328
+
1329
+ ```bash
1330
+ # OTLP endpoint (where traces are sent)
1331
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
1332
+
1333
+ # Service name (shown in traces)
1334
+ export OTEL_SERVICE_NAME=production-api
1335
+
1336
+ # Sampling (reduce overhead in high-traffic scenarios)
1337
+ export OTEL_TRACES_SAMPLER=traceidratio
1338
+ export OTEL_TRACES_SAMPLER_ARG=0.1 # Sample 10% of traces
1339
+ ```
1340
+
1341
+ Or in code:
1342
+
1343
+ ```python
1344
+ status = setup_observability(
1345
+ service_name="my-service",
1346
+ enable_tracing=True,
1347
+ enable_metrics=True,
1348
+ metrics_port=9090,
1349
+ metrics_host="0.0.0.0" # Allow external Prometheus scraping
1350
+ )
1351
+
1352
+ # Check status
1353
+ if status["tracing_enabled"]:
1354
+ print("Traces exporting to OTLP endpoint")
1355
+ if status["metrics_server_started"]:
1356
+ print("Metrics available at http://localhost:9090/metrics")
1357
+ ```
1358
+
1359
+ **Production Integration**
1360
+
1361
+ **With Grafana + Prometheus:**
1362
+ ```yaml
1363
+ # prometheus.yml
1364
+ scrape_configs:
1365
+ - job_name: 'chuk-tool-processor'
1366
+ scrape_interval: 15s
1367
+ static_configs:
1368
+ - targets: ['app:9090']
1369
+ ```
1370
+
1371
+ **With OpenTelemetry Collector:**
1372
+ ```yaml
1373
+ # otel-collector-config.yaml
1374
+ receivers:
1375
+ otlp:
1376
+ protocols:
1377
+ grpc:
1378
+ endpoint: 0.0.0.0:4317
1379
+
1380
+ exporters:
1381
+ jaeger:
1382
+ endpoint: jaeger:14250
1383
+ prometheus:
1384
+ endpoint: 0.0.0.0:8889
1385
+
1386
+ service:
1387
+ pipelines:
1388
+ traces:
1389
+ receivers: [otlp]
1390
+ exporters: [jaeger]
1391
+ ```
1392
+
1393
+ **With Cloud Providers:**
1394
+ ```bash
1395
+ # AWS X-Ray
1396
+ export OTEL_TRACES_SAMPLER=xray
1397
+
1398
+ # Google Cloud Trace
1399
+ export OTEL_EXPORTER_OTLP_ENDPOINT=https://cloudtrace.googleapis.com/v1/projects/PROJECT_ID/traces
1400
+
1401
+ # Datadog
1402
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://datadog-agent:4317
1403
+ ```
1404
+
1405
+ **Why This Matters**
1406
+
1407
+ ❌ **Without telemetry:**
1408
+ - "Why is this tool slow?" → No idea
1409
+ - "Is caching helping?" → Guessing
1410
+ - "Did that retry work?" → Check logs manually
1411
+ - "Is the circuit breaker working?" → Hope so
1412
+ - "Which tool is failing?" → Debug blindly
1413
+
1414
+ ✅ **With telemetry:**
1415
+ - See exact execution timeline in Jaeger
1416
+ - Monitor cache hit rate in Grafana
1417
+ - Alert when retry rate spikes
1418
+ - Dashboard shows circuit breaker states
1419
+ - Metrics pinpoint the failing tool immediately
1420
+
1421
+ **Learn More**
1422
+
1423
+ 📖 **Complete Guide**: See [`OBSERVABILITY.md`](OBSERVABILITY.md) for:
1424
+ - Complete span and metric specifications
1425
+ - Architecture and implementation details
1426
+ - Integration guides (Jaeger, Grafana, OTEL Collector)
1427
+ - Testing observability features
1428
+ - Environment variable configuration
1429
+
1430
+ 🎯 **Working Example**: See `examples/observability_demo.py` for a complete demonstration with retries, caching, and circuit breakers
1431
+
1432
+ **Benefits**
1433
+
1434
+ ✅ **Drop-in** - One function call, zero code changes
1435
+ ✅ **Automatic** - All execution layers instrumented
1436
+ ✅ **Standard** - OTEL + Prometheus (works with existing tools)
1437
+ ✅ **Production-ready** - Ops teams get exactly what they expect
1438
+ ✅ **Optional** - Gracefully degrades if packages not installed
1439
+ ✅ **Zero-overhead** - No performance impact when disabled
1440
+
1441
+ ### Error Handling
1442
+
1443
+ ```python
1444
+ results = await processor.process(llm_output)
1445
+
1446
+ for result in results:
1447
+ if result.error:
1448
+ print(f"Tool '{result.tool}' failed: {result.error}")
1449
+ print(f"Duration: {result.duration}s")
1450
+ else:
1451
+ print(f"Tool '{result.tool}' succeeded: {result.result}")
1452
+ ```
1453
+
1454
+ ### Testing Tools
1455
+
1456
+ ```python
1457
+ import pytest
1458
+ from chuk_tool_processor.core.processor import ToolProcessor
1459
+ from chuk_tool_processor.registry import initialize
1460
+
1461
+ @pytest.mark.asyncio
1462
+ async def test_calculator():
1463
+ await initialize()
1464
+ processor = ToolProcessor()
1465
+
1466
+ results = await processor.process(
1467
+ '<tool name="calculator" args=\'{"operation": "add", "a": 5, "b": 3}\'/>'
1468
+ )
1469
+
1470
+ assert results[0].result["result"] == 8
1471
+ ```
1472
+
1473
+ ## Configuration
1474
+
1475
+ ### Timeout Configuration
1476
+
1477
+ CHUK Tool Processor uses a unified timeout configuration system that applies to all MCP transports (HTTP Streamable, SSE, STDIO) and the StreamManager. Instead of managing dozens of individual timeout values, there are just **4 logical timeout categories**:
1478
+
1479
+ ```python
1480
+ from chuk_tool_processor.mcp.transport import TimeoutConfig
1481
+
1482
+ # Create custom timeout configuration
1483
+ timeout_config = TimeoutConfig(
1484
+ connect=30.0, # Connection establishment, initialization, session discovery
1485
+ operation=30.0, # Normal operations (tool calls, listing tools/resources/prompts)
1486
+ quick=5.0, # Fast health checks and pings
1487
+ shutdown=2.0 # Cleanup and shutdown operations
1488
+ )
1489
+ ```
1490
+
1491
+ **Using timeout configuration with StreamManager:**
1492
+
1493
+ ```python
1494
+ from chuk_tool_processor.mcp.stream_manager import StreamManager
1495
+ from chuk_tool_processor.mcp.transport import TimeoutConfig
1496
+
1497
+ # Create StreamManager with custom timeouts
1498
+ timeout_config = TimeoutConfig(
1499
+ connect=60.0, # Longer for slow initialization
1500
+ operation=45.0, # Longer for heavy operations
1501
+ quick=3.0, # Faster health checks
1502
+ shutdown=5.0 # More time for cleanup
1503
+ )
1504
+
1505
+ manager = StreamManager(timeout_config=timeout_config)
1506
+ ```
1507
+
1508
+ **Timeout categories explained:**
1509
+
1510
+ | Category | Default | Used For | Examples |
1511
+ |----------|---------|----------|----------|
1512
+ | `connect` | 30.0s | Connection setup, initialization, discovery | HTTP connection, SSE session discovery, STDIO subprocess launch |
1513
+ | `operation` | 30.0s | Normal tool operations | Tool calls, listing tools/resources/prompts, get_tools() |
1514
+ | `quick` | 5.0s | Fast health/status checks | Ping operations, health checks |
1515
+ | `shutdown` | 2.0s | Cleanup and teardown | Transport close, connection cleanup |
1516
+
1517
+ **Why this matters:**
1518
+ - ✅ **Simple**: 4 timeout values instead of 20+
1519
+ - ✅ **Consistent**: Same timeout behavior across all transports
1520
+ - ✅ **Configurable**: Adjust timeouts based on your environment (slow networks, large datasets, etc.)
1521
+ - ✅ **Type-safe**: Pydantic validation ensures correct values
1522
+
1523
+ **Example: Adjusting for slow environments**
1524
+
1525
+ ```python
1526
+ from chuk_tool_processor.mcp import setup_mcp_stdio
1527
+ from chuk_tool_processor.mcp.transport import TimeoutConfig
1528
+
1529
+ # For slow network or resource-constrained environments
1530
+ slow_timeouts = TimeoutConfig(
1531
+ connect=120.0, # Allow more time for package downloads
1532
+ operation=60.0, # Allow more time for heavy operations
1533
+ quick=10.0, # Be patient with health checks
1534
+ shutdown=10.0 # Allow thorough cleanup
1535
+ )
1536
+
1537
+ processor, manager = await setup_mcp_stdio(
1538
+ config_file="mcp_config.json",
1539
+ servers=["sqlite"],
1540
+ namespace="db",
1541
+ initialization_timeout=120.0
1542
+ )
1543
+
1544
+ # Set custom timeouts on the manager
1545
+ manager.timeout_config = slow_timeouts
1546
+ ```
1547
+
1548
+ ### Environment Variables
1549
+
1550
+ | Variable | Default | Description |
1551
+ |----------|---------|-------------|
1552
+ | `CHUK_TOOL_REGISTRY_PROVIDER` | `memory` | Registry backend |
1553
+ | `CHUK_DEFAULT_TIMEOUT` | `30.0` | Default timeout (seconds) |
1554
+ | `CHUK_LOG_LEVEL` | `INFO` | Logging level |
1555
+ | `CHUK_STRUCTURED_LOGGING` | `true` | Enable JSON logging |
1556
+ | `MCP_BEARER_TOKEN` | - | Bearer token for MCP SSE |
1557
+
1558
+ ### ToolProcessor Options
1559
+
1560
+ ```python
1561
+ processor = ToolProcessor(
1562
+ default_timeout=30.0, # Timeout per tool
1563
+ max_concurrency=10, # Max concurrent executions
1564
+ enable_caching=True, # Result caching
1565
+ cache_ttl=300, # Cache TTL (seconds)
1566
+ enable_rate_limiting=False, # Rate limiting
1567
+ global_rate_limit=None, # (requests per minute) global cap
1568
+ enable_retries=True, # Auto-retry failures
1569
+ max_retries=3, # Max retry attempts
1570
+ # Optional per-tool rate limits: {"tool.name": (requests, per_seconds)}
1571
+ tool_rate_limits=None
1572
+ )
1573
+ ```
1574
+
1575
+ ### Performance & Tuning
1576
+
1577
+ | Parameter | Default | When to Adjust |
1578
+ |-----------|---------|----------------|
1579
+ | `default_timeout` | `30.0` | Increase for slow tools (e.g., AI APIs) |
1580
+ | `max_concurrency` | `10` | Increase for I/O-bound tools, decrease for CPU-bound |
1581
+ | `enable_caching` | `True` | Keep on for deterministic tools |
1582
+ | `cache_ttl` | `300` | Longer for stable data, shorter for real-time |
1583
+ | `enable_rate_limiting` | `False` | Enable when hitting API rate limits |
1584
+ | `global_rate_limit` | `None` | Set a global requests/min cap across all tools |
1585
+ | `enable_retries` | `True` | Disable for non-idempotent operations |
1586
+ | `max_retries` | `3` | Increase for flaky external APIs |
1587
+ | `tool_rate_limits` | `None` | Dict mapping tool name → (max_requests, window_seconds). Overrides `global_rate_limit` per tool |
1588
+
1589
+ **Per-tool rate limiting example:**
1590
+
1591
+ ```python
1592
+ processor = ToolProcessor(
1593
+ enable_rate_limiting=True,
1594
+ global_rate_limit=100, # 100 requests/minute across all tools
1595
+ tool_rate_limits={
1596
+ "notion.search_pages": (10, 60), # 10 requests per 60 seconds
1597
+ "expensive_api": (5, 60), # 5 requests per minute
1598
+ "local_tool": (1000, 60), # 1000 requests per minute (local is fast)
1599
+ }
1600
+ )
1601
+ ```
1602
+
1603
+ ### Security Model
1604
+
1605
+ CHUK Tool Processor provides multiple layers of safety:
1606
+
1607
+ | Concern | Protection | Configuration |
1608
+ |---------|------------|---------------|
1609
+ | **Timeouts** | Every tool has a timeout | `default_timeout=30.0` |
1610
+ | **Process Isolation** | Run tools in separate processes | `strategy=SubprocessStrategy()` |
1611
+ | **Rate Limiting** | Prevent abuse and API overuse | `enable_rate_limiting=True` |
1612
+ | **Input Validation** | Pydantic validation on arguments | Use `ValidatedTool` |
1613
+ | **Error Containment** | Failures don't crash the processor | Built-in exception handling |
1614
+ | **Retry Limits** | Prevent infinite retry loops | `max_retries=3` |
1615
+
1616
+ **Important Security Notes:**
1617
+ - **Environment Variables**: Subprocess strategy inherits the parent process environment by default. For stricter isolation, use container-level controls (Docker, cgroups).
1618
+ - **Network Access**: Tools inherit network access from the host. For network isolation, use OS-level sandboxing (containers, network namespaces, firewalls).
1619
+ - **Resource Limits**: For hard CPU/memory caps, use OS-level controls (cgroups on Linux, Job Objects on Windows, or Docker resource limits).
1620
+ - **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
+
1622
+ Example security-focused setup for untrusted code:
1623
+
1624
+ ```python
1625
+ 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
1629
+
1630
+ async def create_secure_processor():
1631
+ # Maximum isolation for untrusted code
1632
+ # Runs each tool in a separate process
1633
+ registry = await get_default_registry()
1634
+
1635
+ processor = ToolProcessor(
1636
+ strategy=SubprocessStrategy(
1637
+ registry=registry,
1638
+ max_workers=4,
1639
+ default_timeout=10.0
1640
+ ),
1641
+ default_timeout=10.0,
1642
+ enable_rate_limiting=True,
1643
+ global_rate_limit=50, # 50 requests/minute
1644
+ max_retries=2
1645
+ )
1646
+ return processor
1647
+
1648
+ # For even stricter isolation:
1649
+ # - Run the entire processor inside a Docker container with resource limits
1650
+ # - Use network policies to restrict outbound connections
1651
+ # - Use read-only filesystems where possible
1652
+ ```
1653
+
1654
+ ## Architecture Principles
1655
+
1656
+ 1. **Composability**: Stack strategies and wrappers like middleware
1657
+ 2. **Async-First**: Built for `async/await` from the ground up
1658
+ 3. **Production-Ready**: Timeouts, retries, caching, rate limiting—all built-in
1659
+ 4. **Pluggable**: Parsers, strategies, transports—swap components as needed
1660
+ 5. **Observable**: Structured logging and metrics collection throughout
1661
+
1662
+ ## Examples
1663
+
1664
+ Check out the [`examples/`](examples/) directory for complete working examples:
1665
+
1666
+ ### 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
1675
+
1676
+ ### MCP Integration (Real-World)
1677
+ - **Notion + OAuth**: `examples/notion_oauth.py` - Complete OAuth 2.1 flow with HTTP Streamable
1678
+ - Shows: Authorization Server discovery, client registration, PKCE flow, token exchange
1679
+ - **SQLite Local**: `examples/stdio_sqlite.py` - Local database access via STDIO
1680
+ - Shows: Command/args passing, environment variables, file paths, initialization timeouts
1681
+ - **Echo Server**: `examples/stdio_echo.py` - Minimal STDIO transport example
1682
+ - Shows: Simplest possible MCP integration for testing
1683
+ - **Atlassian + OAuth**: `examples/atlassian_sse.py` - OAuth with SSE transport (legacy)
1684
+
1685
+ ### 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`
1690
+
1691
+ ## FAQ
1692
+
1693
+ **Q: What happens if a tool takes too long?**
1694
+ A: The tool is cancelled after `default_timeout` seconds and returns an error result. The processor continues with other tools.
1695
+
1696
+ **Q: Can I mix local and remote (MCP) tools?**
1697
+ A: Yes! Register local tools first, then use `setup_mcp_*` to add remote tools. They all work in the same processor.
1698
+
1699
+ **Q: How do I handle malformed LLM outputs?**
1700
+ A: The processor is resilient—invalid tool calls are logged and return error results without crashing.
1701
+
1702
+ **Q: What about API rate limits?**
1703
+ A: Use `enable_rate_limiting=True` and set `tool_rate_limits` per tool or `global_rate_limit` for all tools.
1704
+
1705
+ **Q: Can tools return files or binary data?**
1706
+ A: Yes—tools can return any JSON-serializable data including base64-encoded files, URLs, or structured data.
1707
+
1708
+ **Q: How do I test my tools?**
1709
+ A: Use pytest with `@pytest.mark.asyncio`. See [Testing Tools](#testing-tools) for examples.
1710
+
1711
+ **Q: Does this work with streaming LLM responses?**
1712
+ A: Yes—as tool calls appear in the stream, extract and process them. The processor handles partial/incremental tool call lists.
1713
+
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.
1716
+
1717
+ ## Comparison with Other Tools
1718
+
1719
+ | Feature | chuk-tool-processor | LangChain Tools | OpenAI Tools | MCP SDK |
1720
+ |---------|-------------------|-----------------|--------------|---------|
1721
+ | **Async-native** | ✅ | ⚠️ Partial | ✅ | ✅ |
1722
+ | **Process isolation** | ✅ SubprocessStrategy | ❌ | ❌ | ⚠️ |
1723
+ | **Built-in retries** | ✅ | ❌ † | ❌ | ❌ |
1724
+ | **Rate limiting** | ✅ | ❌ † | ⚠️ ‡ | ❌ |
1725
+ | **Caching** | ✅ | ⚠️ † | ❌ ‡ | ❌ |
1726
+ | **Multiple parsers** | ✅ (XML, OpenAI, JSON) | ⚠️ | ✅ | ✅ |
1727
+ | **Streaming tools** | ✅ | ⚠️ | ⚠️ | ✅ |
1728
+ | **MCP integration** | ✅ All transports | ❌ | ❌ | ✅ (protocol only) |
1729
+ | **Zero-config start** | ✅ | ❌ | ✅ | ⚠️ |
1730
+ | **Production-ready** | ✅ Timeouts, metrics | ⚠️ | ⚠️ | ⚠️ |
1731
+
1732
+ **Notes:**
1733
+ - † LangChain offers caching and rate-limiting through separate libraries (`langchain-cache`, external rate limiters), but they're not core features.
1734
+ - ‡ OpenAI Tools can be combined with external rate limiters and caches, but tool execution itself doesn't include these features.
1735
+
1736
+ **When to use chuk-tool-processor:**
1737
+ - You need production-ready tool execution (timeouts, retries, caching)
1738
+ - You want to connect to MCP servers (local or remote)
1739
+ - You need to run untrusted code safely (subprocess isolation)
1740
+ - You're building a custom LLM application (not using a framework)
1741
+
1742
+ **When to use alternatives:**
1743
+ - **LangChain**: You want a full-featured LLM framework with chains, agents, and memory
1744
+ - **OpenAI Tools**: You only use OpenAI and don't need advanced execution features
1745
+ - **MCP SDK**: You're building an MCP server, not a client
1746
+
1747
+ ## Related Projects
1748
+
1749
+ - **[chuk-mcp](https://github.com/chrishayuk/chuk-mcp)**: Low-level Model Context Protocol client
1750
+ - Powers the MCP transport layer in chuk-tool-processor
1751
+ - Use directly if you need protocol-level control
1752
+ - Use chuk-tool-processor if you want high-level tool execution
1753
+
1754
+ ## Development & Publishing
1755
+
1756
+ ### For Contributors
1757
+
1758
+ Development setup:
1759
+
1760
+ ```bash
1761
+ # Clone repository
1762
+ git clone https://github.com/chrishayuk/chuk-tool-processor.git
1763
+ cd chuk-tool-processor
1764
+
1765
+ # Install development dependencies
1766
+ uv sync --dev
1767
+
1768
+ # Run tests
1769
+ make test
1770
+
1771
+ # Run all quality checks
1772
+ make check
1773
+ ```
1774
+
1775
+ ### For Maintainers: Publishing Releases
1776
+
1777
+ The project uses **fully automated CI/CD** for releases. Publishing is as simple as:
1778
+
1779
+ ```bash
1780
+ # 1. Bump version
1781
+ make bump-patch # or bump-minor, bump-major
1782
+
1783
+ # 2. Commit version change
1784
+ git add pyproject.toml
1785
+ git commit -m "version X.Y.Z"
1786
+ git push
1787
+
1788
+ # 3. Create release (automated)
1789
+ make publish
1790
+ ```
1791
+
1792
+ This will:
1793
+ - Create and push a git tag
1794
+ - Trigger GitHub Actions to create a release with auto-generated changelog
1795
+ - Run tests across all platforms and Python versions
1796
+ - Build and publish to PyPI automatically
1797
+
1798
+ For detailed release documentation, see:
1799
+ - **[RELEASING.md](RELEASING.md)** - Complete release process guide
1800
+ - **[docs/CI-CD.md](docs/CI-CD.md)** - Full CI/CD pipeline documentation
1801
+
1802
+ ## Contributing & Support
1803
+
1804
+ - **GitHub**: [chrishayuk/chuk-tool-processor](https://github.com/chrishayuk/chuk-tool-processor)
1805
+ - **Issues**: [Report bugs and request features](https://github.com/chrishayuk/chuk-tool-processor/issues)
1806
+ - **Discussions**: [Community discussions](https://github.com/chrishayuk/chuk-tool-processor/discussions)
1807
+ - **License**: MIT
1808
+
1809
+ ---
1810
+
1811
+ **Remember**: CHUK Tool Processor is the missing link between LLM outputs and reliable tool execution. It's not trying to be everything—it's trying to be the best at one thing: processing tool calls in production.
1812
+
1813
+ Built with ❤️ by the CHUK AI team for the LLM tool integration community.