chuk-tool-processor 0.10__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 (75) hide show
  1. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/PKG-INFO +153 -20
  2. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/README.md +152 -19
  3. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/pyproject.toml +3 -1
  4. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/__init__.py +5 -2
  5. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/core/exceptions.py +55 -4
  6. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/core/processor.py +5 -5
  7. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/__init__.py +2 -1
  8. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/models.py +65 -1
  9. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +50 -11
  10. chuk_tool_processor-0.11/src/chuk_tool_processor/registry/__init__.py +110 -0
  11. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/decorators.py +42 -0
  12. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/interface.py +8 -3
  13. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/metadata.py +26 -0
  14. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/providers/memory.py +23 -7
  15. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/tool_export.py +19 -17
  16. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/PKG-INFO +153 -20
  17. chuk_tool_processor-0.10/src/chuk_tool_processor/registry/__init__.py +0 -60
  18. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/setup.cfg +0 -0
  19. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/core/__init__.py +0 -0
  20. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/__init__.py +0 -0
  21. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
  22. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +0 -0
  23. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +0 -0
  24. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
  25. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
  26. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
  27. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/circuit_breaker.py +0 -0
  28. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
  29. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
  30. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/__init__.py +0 -0
  31. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/context.py +0 -0
  32. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/formatter.py +0 -0
  33. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/helpers.py +0 -0
  34. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/metrics.py +0 -0
  35. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/mcp_tool.py +0 -0
  36. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
  37. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
  38. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
  39. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/stream_manager.py +0 -0
  40. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
  41. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
  42. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +0 -0
  43. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/models.py +0 -0
  44. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/sse_transport.py +0 -0
  45. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -0
  46. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/__init__.py +0 -0
  47. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
  48. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
  49. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_call.py +0 -0
  50. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_export_mixin.py +0 -0
  51. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_result.py +0 -0
  52. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_spec.py +0 -0
  53. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/validated_tool.py +0 -0
  54. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/__init__.py +0 -0
  55. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/metrics.py +0 -0
  56. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/setup.py +0 -0
  57. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/tracing.py +0 -0
  58. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/__init__.py +0 -0
  59. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/discovery.py +0 -0
  60. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
  61. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
  62. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
  63. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
  64. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
  65. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
  66. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/py.typed +0 -0
  67. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/auto_register.py +0 -0
  68. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/provider.py +0 -0
  69. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/providers/__init__.py +0 -0
  70. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/utils/__init__.py +0 -0
  71. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/utils/validation.py +0 -0
  72. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/SOURCES.txt +0 -0
  73. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
  74. {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/requires.txt +0 -0
  75. {chuk_tool_processor-0.10 → 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.10
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>
@@ -96,15 +96,15 @@ Works with OpenAI, Anthropic, local models (Ollama/MLX/vLLM), and any framework
96
96
 
97
97
  ```python
98
98
  import asyncio
99
- from chuk_tool_processor import ToolProcessor, register_tool, initialize
99
+ from chuk_tool_processor import ToolProcessor, tool
100
100
 
101
- @register_tool(name="weather")
101
+ @tool(name="weather") # Clean decorator syntax
102
102
  class WeatherTool:
103
103
  async def execute(self, city: str) -> dict:
104
104
  return {"temp": 72, "condition": "sunny", "city": city}
105
105
 
106
106
  async def main():
107
- await initialize()
107
+ # No need for initialize() - auto-initializes on first use!
108
108
  async with ToolProcessor(enable_caching=True, enable_retries=True) as p:
109
109
  # Works with OpenAI, Anthropic, or JSON formats
110
110
  result = await p.process('<tool name="weather" args=\'{"city": "SF"}\'/>')
@@ -374,10 +374,10 @@ Copy-paste this into a file and run it:
374
374
 
375
375
  ```python
376
376
  import asyncio
377
- from chuk_tool_processor import ToolProcessor, register_tool, initialize
377
+ from chuk_tool_processor import ToolProcessor, tool
378
378
 
379
- # Step 1: Define a tool
380
- @register_tool(name="calculator")
379
+ # Step 1: Define a tool with the clean @tool decorator
380
+ @tool(name="calculator")
381
381
  class Calculator:
382
382
  async def execute(self, operation: str, a: float, b: float) -> dict:
383
383
  ops = {"add": a + b, "multiply": a * b, "subtract": a - b}
@@ -387,7 +387,7 @@ class Calculator:
387
387
 
388
388
  # Step 2: Process LLM output
389
389
  async def main():
390
- await initialize()
390
+ # No initialize() needed - it auto-initializes!
391
391
 
392
392
  # Use context manager for automatic cleanup
393
393
  async with ToolProcessor() as processor:
@@ -412,10 +412,87 @@ asyncio.run(main())
412
412
  - ✅ Automatic timeouts, retries, and caching
413
413
  - ✅ Clean resource management (context manager)
414
414
  - ✅ Full type checking support
415
+ - ✅ Auto-initialization (no boilerplate!)
415
416
 
416
417
  > **Why not just use OpenAI tool calls?**
417
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.
418
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
+
419
496
  ## Quick Decision Tree (Commit This to Memory)
420
497
 
421
498
  ```
@@ -425,8 +502,8 @@ asyncio.run(main())
425
502
  │ ⚠️ No → IsolatedStrategy (sandboxed) │
426
503
  │ │
427
504
  │ Where do your tools live? │
428
- │ 📦 Local → @register_tool
429
- │ 🌐 Remote → setup_mcp_http_streamable
505
+ │ 📦 Local → @tool decorator
506
+ │ 🌐 Remote → setup_mcp_* with MCPConfig
430
507
  ╰──────────────────────────────────────────╯
431
508
  ```
432
509
 
@@ -436,22 +513,25 @@ asyncio.run(main())
436
513
 
437
514
  Understanding the lifecycle helps you use CHUK Tool Processor correctly:
438
515
 
439
- 1. **`await initialize()`** loads the global registry; call **once per process** at application startup
516
+ 1. **Auto-initialization**Registry auto-initializes on first access (or call `await initialize()` explicitly)
440
517
  2. Create a **`ToolProcessor(...)`** (or use the one returned by `setup_mcp_*`)
441
518
  3. Use **`async with ToolProcessor() as p:`** to ensure cleanup
442
519
  4. **`setup_mcp_*`** returns `(processor, manager)` — reuse that `processor`
443
520
  5. If you need a custom registry, pass it explicitly to the strategy
444
521
  6. You rarely need `get_default_registry()` unless you're composing advanced setups
445
522
 
446
- **⚠️ Important:** `initialize()` must run **once per process**, not once per request or processor instance. Running it multiple times will duplicate tools in the registry.
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!
447
524
 
448
525
  ```python
449
- # Standard pattern
450
- await initialize() # Step 1: Register tools
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
451
530
 
452
- async with ToolProcessor() as p: # Step 2-3: Create + auto cleanup
531
+ # Traditional explicit pattern (still works)
532
+ await initialize() # Explicit initialization
533
+ async with ToolProcessor() as p:
453
534
  results = await p.process(llm_output)
454
- # Step 4: Processor automatically cleaned up on exit
455
535
  ```
456
536
 
457
537
  ## Production Features by Example
@@ -618,7 +698,41 @@ asyncio.run(main())
618
698
 
619
699
  See `examples/04_mcp_integration/notion_oauth.py` for complete OAuth flow.
620
700
 
621
- **Pattern 3: Local SQLite database via STDIO**
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>
735
+
622
736
  ```python
623
737
  import asyncio
624
738
  import json
@@ -643,7 +757,7 @@ async def main():
643
757
  config_file="mcp_config.json",
644
758
  servers=["sqlite"],
645
759
  namespace="db",
646
- initialization_timeout=120.0 # First run downloads the package
760
+ initialization_timeout=120.0
647
761
  )
648
762
 
649
763
  # Query your local database via MCP
@@ -654,6 +768,7 @@ async def main():
654
768
 
655
769
  asyncio.run(main())
656
770
  ```
771
+ </details>
657
772
 
658
773
  See `examples/04_mcp_integration/stdio_sqlite.py` for complete working example.
659
774
 
@@ -937,7 +1052,11 @@ register_fn_tool(get_current_time, namespace="utilities")
937
1052
  For production tools, use Pydantic validation:
938
1053
 
939
1054
  ```python
940
- @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
941
1060
  class WeatherTool(ValidatedTool):
942
1061
  class Arguments(BaseModel):
943
1062
  location: str = Field(..., description="City name")
@@ -951,14 +1070,28 @@ class WeatherTool(ValidatedTool):
951
1070
  return self.Result(temperature=22.5, conditions="Sunny")
952
1071
  ```
953
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
+
954
1085
  #### StreamingTool (Real-time Results)
955
1086
 
956
1087
  For long-running operations that produce incremental results:
957
1088
 
958
1089
  ```python
1090
+ from chuk_tool_processor import tool
959
1091
  from chuk_tool_processor.models import StreamingTool
1092
+ from pydantic import BaseModel
960
1093
 
961
- @register_tool(name="file_processor")
1094
+ @tool(name="file_processor") # Clean @tool decorator
962
1095
  class FileProcessor(StreamingTool):
963
1096
  class Arguments(BaseModel):
964
1097
  file_path: str
@@ -68,15 +68,15 @@ Works with OpenAI, Anthropic, local models (Ollama/MLX/vLLM), and any framework
68
68
 
69
69
  ```python
70
70
  import asyncio
71
- from chuk_tool_processor import ToolProcessor, register_tool, initialize
71
+ from chuk_tool_processor import ToolProcessor, tool
72
72
 
73
- @register_tool(name="weather")
73
+ @tool(name="weather") # Clean decorator syntax
74
74
  class WeatherTool:
75
75
  async def execute(self, city: str) -> dict:
76
76
  return {"temp": 72, "condition": "sunny", "city": city}
77
77
 
78
78
  async def main():
79
- await initialize()
79
+ # No need for initialize() - auto-initializes on first use!
80
80
  async with ToolProcessor(enable_caching=True, enable_retries=True) as p:
81
81
  # Works with OpenAI, Anthropic, or JSON formats
82
82
  result = await p.process('<tool name="weather" args=\'{"city": "SF"}\'/>')
@@ -346,10 +346,10 @@ Copy-paste this into a file and run it:
346
346
 
347
347
  ```python
348
348
  import asyncio
349
- from chuk_tool_processor import ToolProcessor, register_tool, initialize
349
+ from chuk_tool_processor import ToolProcessor, tool
350
350
 
351
- # Step 1: Define a tool
352
- @register_tool(name="calculator")
351
+ # Step 1: Define a tool with the clean @tool decorator
352
+ @tool(name="calculator")
353
353
  class Calculator:
354
354
  async def execute(self, operation: str, a: float, b: float) -> dict:
355
355
  ops = {"add": a + b, "multiply": a * b, "subtract": a - b}
@@ -359,7 +359,7 @@ class Calculator:
359
359
 
360
360
  # Step 2: Process LLM output
361
361
  async def main():
362
- await initialize()
362
+ # No initialize() needed - it auto-initializes!
363
363
 
364
364
  # Use context manager for automatic cleanup
365
365
  async with ToolProcessor() as processor:
@@ -384,10 +384,87 @@ asyncio.run(main())
384
384
  - ✅ Automatic timeouts, retries, and caching
385
385
  - ✅ Clean resource management (context manager)
386
386
  - ✅ Full type checking support
387
+ - ✅ Auto-initialization (no boilerplate!)
387
388
 
388
389
  > **Why not just use OpenAI tool calls?**
389
390
  > 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.
390
391
 
392
+ ### Enhanced Developer Experience
393
+
394
+ CHUK Tool Processor provides intuitive APIs and helpful error messages:
395
+
396
+ **1. Clean Decorator Syntax**
397
+ ```python
398
+ from chuk_tool_processor import tool
399
+
400
+ @tool(name="calculator") # Short and clean!
401
+ class Calculator:
402
+ async def execute(self, a: int, b: int) -> int:
403
+ return a + b
404
+ ```
405
+
406
+ **2. Auto-Initialization (No Boilerplate)**
407
+ ```python
408
+ from chuk_tool_processor import ToolProcessor
409
+
410
+ # No initialize() needed - it auto-initializes!
411
+ async with ToolProcessor() as p:
412
+ results = await p.process(llm_output)
413
+ ```
414
+
415
+ **3. Type-Safe Tool Discovery**
416
+ ```python
417
+ from chuk_tool_processor import get_default_registry, ToolInfo
418
+
419
+ registry = await get_default_registry()
420
+
421
+ # List all registered tools with clear, typed results
422
+ tools = await registry.list_tools()
423
+ for tool in tools: # Each tool is a ToolInfo object
424
+ print(f"{tool.namespace}:{tool.name}") # Clear attribute access!
425
+ # No more confusing tuple unpacking: (namespace, name) vs (name, namespace)?
426
+ ```
427
+
428
+ **4. Helpful Error Messages**
429
+ ```python
430
+ # Typo in tool name? Get helpful suggestions!
431
+ try:
432
+ await registry.get_tool_strict("calcuator", namespace="default")
433
+ except Exception as e:
434
+ print(e)
435
+ # Output:
436
+ # Tool 'calcuator' not found in namespace 'default'
437
+ #
438
+ # Did you mean: calculator?
439
+ #
440
+ # Available namespaces: default, math, mcp
441
+ #
442
+ # Tip: Use `await registry.list_tools()` to see all registered tools
443
+ ```
444
+
445
+ **5. Clean MCP Configuration**
446
+ ```python
447
+ from chuk_tool_processor.mcp import setup_mcp_stdio, MCPConfig, MCPServerConfig
448
+
449
+ # Clean Pydantic config object instead of 14+ parameters!
450
+ processor, manager = await setup_mcp_stdio(
451
+ config=MCPConfig(
452
+ servers=[MCPServerConfig(name="echo", command="uvx", args=["mcp-echo"])],
453
+ namespace="tools",
454
+ enable_caching=True,
455
+ cache_ttl=600,
456
+ )
457
+ )
458
+ ```
459
+
460
+ **Key improvements:**
461
+ - ✅ **`@tool` decorator**: Shorter, cleaner than `@register_tool`
462
+ - ✅ **Auto-initialization**: No need for explicit `initialize()` calls
463
+ - ✅ **Type-safe tool listing**: `ToolInfo` objects instead of confusing tuples
464
+ - ✅ **Helpful errors**: Fuzzy matching suggestions when tools aren't found
465
+ - ✅ **MCPConfig**: Clean Pydantic model instead of 14+ parameters
466
+ - ✅ **Better discoverability**: Clear guidance on how to explore available tools
467
+
391
468
  ## Quick Decision Tree (Commit This to Memory)
392
469
 
393
470
  ```
@@ -397,8 +474,8 @@ asyncio.run(main())
397
474
  │ ⚠️ No → IsolatedStrategy (sandboxed) │
398
475
  │ │
399
476
  │ Where do your tools live? │
400
- │ 📦 Local → @register_tool
401
- │ 🌐 Remote → setup_mcp_http_streamable
477
+ │ 📦 Local → @tool decorator
478
+ │ 🌐 Remote → setup_mcp_* with MCPConfig
402
479
  ╰──────────────────────────────────────────╯
403
480
  ```
404
481
 
@@ -408,22 +485,25 @@ asyncio.run(main())
408
485
 
409
486
  Understanding the lifecycle helps you use CHUK Tool Processor correctly:
410
487
 
411
- 1. **`await initialize()`** loads the global registry; call **once per process** at application startup
488
+ 1. **Auto-initialization**Registry auto-initializes on first access (or call `await initialize()` explicitly)
412
489
  2. Create a **`ToolProcessor(...)`** (or use the one returned by `setup_mcp_*`)
413
490
  3. Use **`async with ToolProcessor() as p:`** to ensure cleanup
414
491
  4. **`setup_mcp_*`** returns `(processor, manager)` — reuse that `processor`
415
492
  5. If you need a custom registry, pass it explicitly to the strategy
416
493
  6. You rarely need `get_default_registry()` unless you're composing advanced setups
417
494
 
418
- **⚠️ Important:** `initialize()` must run **once per process**, not once per request or processor instance. Running it multiple times will duplicate tools in the registry.
495
+ **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!
419
496
 
420
497
  ```python
421
- # Standard pattern
422
- await initialize() # Step 1: Register tools
498
+ # New simplified pattern (auto-initialization)
499
+ async with ToolProcessor() as p: # Auto-initializes on first use!
500
+ results = await p.process(llm_output)
501
+ # Processor automatically cleaned up on exit
423
502
 
424
- async with ToolProcessor() as p: # Step 2-3: Create + auto cleanup
503
+ # Traditional explicit pattern (still works)
504
+ await initialize() # Explicit initialization
505
+ async with ToolProcessor() as p:
425
506
  results = await p.process(llm_output)
426
- # Step 4: Processor automatically cleaned up on exit
427
507
  ```
428
508
 
429
509
  ## Production Features by Example
@@ -590,7 +670,41 @@ asyncio.run(main())
590
670
 
591
671
  See `examples/04_mcp_integration/notion_oauth.py` for complete OAuth flow.
592
672
 
593
- **Pattern 3: Local SQLite database via STDIO**
673
+ **Pattern 3: Local SQLite database via STDIO (New Clean API)**
674
+ ```python
675
+ import asyncio
676
+ from chuk_tool_processor.mcp import setup_mcp_stdio, MCPConfig, MCPServerConfig
677
+
678
+ async def main():
679
+ # NEW: Clean Pydantic config approach (recommended!)
680
+ processor, manager = await setup_mcp_stdio(
681
+ config=MCPConfig(
682
+ servers=[
683
+ MCPServerConfig(
684
+ name="sqlite",
685
+ command="uvx",
686
+ args=["mcp-server-sqlite", "--db-path", "./app.db"],
687
+ )
688
+ ],
689
+ namespace="db",
690
+ initialization_timeout=120.0, # First run downloads the package
691
+ enable_caching=True,
692
+ cache_ttl=600,
693
+ )
694
+ )
695
+
696
+ # Query your local database via MCP
697
+ results = await processor.process(
698
+ '<tool name="db.query" args=\'{"sql": "SELECT * FROM users LIMIT 10"}\'/>'
699
+ )
700
+ print(results[0].result)
701
+
702
+ asyncio.run(main())
703
+ ```
704
+
705
+ <details>
706
+ <summary><strong>Legacy approach (still works)</strong></summary>
707
+
594
708
  ```python
595
709
  import asyncio
596
710
  import json
@@ -615,7 +729,7 @@ async def main():
615
729
  config_file="mcp_config.json",
616
730
  servers=["sqlite"],
617
731
  namespace="db",
618
- initialization_timeout=120.0 # First run downloads the package
732
+ initialization_timeout=120.0
619
733
  )
620
734
 
621
735
  # Query your local database via MCP
@@ -626,6 +740,7 @@ async def main():
626
740
 
627
741
  asyncio.run(main())
628
742
  ```
743
+ </details>
629
744
 
630
745
  See `examples/04_mcp_integration/stdio_sqlite.py` for complete working example.
631
746
 
@@ -909,7 +1024,11 @@ register_fn_tool(get_current_time, namespace="utilities")
909
1024
  For production tools, use Pydantic validation:
910
1025
 
911
1026
  ```python
912
- @register_tool(name="weather")
1027
+ from chuk_tool_processor import tool
1028
+ from chuk_tool_processor.models import ValidatedTool
1029
+ from pydantic import BaseModel, Field
1030
+
1031
+ @tool(name="weather") # Clean @tool decorator
913
1032
  class WeatherTool(ValidatedTool):
914
1033
  class Arguments(BaseModel):
915
1034
  location: str = Field(..., description="City name")
@@ -923,14 +1042,28 @@ class WeatherTool(ValidatedTool):
923
1042
  return self.Result(temperature=22.5, conditions="Sunny")
924
1043
  ```
925
1044
 
1045
+ <details>
1046
+ <summary><strong>Alternative: Using @register_tool (still works)</strong></summary>
1047
+
1048
+ ```python
1049
+ from chuk_tool_processor import register_tool
1050
+
1051
+ @register_tool(name="weather") # Longer form, but identical functionality
1052
+ class WeatherTool(ValidatedTool):
1053
+ # ... same as above
1054
+ ```
1055
+ </details>
1056
+
926
1057
  #### StreamingTool (Real-time Results)
927
1058
 
928
1059
  For long-running operations that produce incremental results:
929
1060
 
930
1061
  ```python
1062
+ from chuk_tool_processor import tool
931
1063
  from chuk_tool_processor.models import StreamingTool
1064
+ from pydantic import BaseModel
932
1065
 
933
- @register_tool(name="file_processor")
1066
+ @tool(name="file_processor") # Clean @tool decorator
934
1067
  class FileProcessor(StreamingTool):
935
1068
  class Arguments(BaseModel):
936
1069
  file_path: str
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "chuk-tool-processor"
7
- version = "0.10"
7
+ version = "0.11"
8
8
  description = "Async-native framework for registering, discovering, and executing tools referenced in LLM responses"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -146,6 +146,8 @@ disallow_incomplete_defs = true # NEW: Require either all or no params typed
146
146
  module = [
147
147
  "chuk_mcp.*",
148
148
  "httpx_sse.*",
149
+ "opentelemetry.*",
150
+ "prometheus_client.*",
149
151
  ]
150
152
  ignore_missing_imports = true
151
153
 
@@ -47,8 +47,9 @@ from chuk_tool_processor.mcp.stream_manager import StreamManager
47
47
  from chuk_tool_processor.models.tool_call import ToolCall
48
48
  from chuk_tool_processor.models.tool_result import ToolResult
49
49
 
50
- # Registry functions
50
+ # Registry functions and types
51
51
  from chuk_tool_processor.registry import (
52
+ ToolInfo,
52
53
  ToolRegistryProvider,
53
54
  get_default_registry,
54
55
  initialize,
@@ -56,7 +57,7 @@ from chuk_tool_processor.registry import (
56
57
  from chuk_tool_processor.registry.auto_register import register_fn_tool
57
58
 
58
59
  # Decorators for registering tools
59
- from chuk_tool_processor.registry.decorators import register_tool
60
+ from chuk_tool_processor.registry.decorators import register_tool, tool
60
61
 
61
62
  # Type checking imports (not available at runtime)
62
63
  if TYPE_CHECKING:
@@ -85,11 +86,13 @@ __all__ = [
85
86
  "ToolCall",
86
87
  "ToolResult",
87
88
  # Registry
89
+ "ToolInfo",
88
90
  "initialize",
89
91
  "get_default_registry",
90
92
  "ToolRegistryProvider",
91
93
  # Decorators
92
94
  "register_tool",
95
+ "tool",
93
96
  "register_fn_tool",
94
97
  # Execution strategies
95
98
  "InProcessStrategy",
@@ -1,6 +1,7 @@
1
1
  # chuk_tool_processor/exceptions.py
2
+ from difflib import get_close_matches
2
3
  from enum import Enum
3
- from typing import Any
4
+ from typing import Any, cast
4
5
 
5
6
 
6
7
  class ErrorCode(str, Enum):
@@ -73,13 +74,63 @@ class ToolProcessorError(Exception):
73
74
  class ToolNotFoundError(ToolProcessorError):
74
75
  """Raised when a requested tool is not found in the registry."""
75
76
 
76
- def __init__(self, tool_name: str, available_tools: list[str] | None = None):
77
+ def __init__(
78
+ self,
79
+ tool_name: str,
80
+ namespace: str = "default",
81
+ available_tools: list[tuple[str, str]] | list[str] | None = None,
82
+ available_namespaces: list[str] | None = None,
83
+ ):
77
84
  self.tool_name = tool_name
78
- details: dict[str, Any] = {"tool_name": tool_name}
85
+ self.namespace = namespace
86
+
87
+ # Build helpful error message
88
+ message_parts = [f"Tool '{tool_name}' not found in namespace '{namespace}'"]
89
+
90
+ # Find similar tool names using fuzzy matching
91
+ similar_tools: list[str] = []
92
+ if available_tools:
93
+ # Handle both tuple format (namespace, name) and string format
94
+ if isinstance(available_tools[0], tuple):
95
+ # Type narrowing: cast to the expected type
96
+ tuple_tools = cast(list[tuple[str, str]], available_tools)
97
+ all_tool_names = [name for _, name in tuple_tools]
98
+ # Also check for namespace:name format
99
+ full_names = [f"{ns}:{name}" for ns, name in tuple_tools]
100
+ similar_in_namespace = get_close_matches(tool_name, all_tool_names, n=3, cutoff=0.6)
101
+ similar_full = get_close_matches(f"{namespace}:{tool_name}", full_names, n=3, cutoff=0.6)
102
+ similar_tools = list(similar_in_namespace) + list(similar_full)
103
+ else:
104
+ # Type narrowing: cast to the expected type
105
+ str_tools = cast(list[str], available_tools)
106
+ similar_tools = list(get_close_matches(tool_name, str_tools, n=3, cutoff=0.6))
107
+
108
+ if similar_tools:
109
+ message_parts.append(f"\n\nDid you mean: {', '.join(similar_tools)}?")
110
+
111
+ # Add available namespaces
112
+ if available_namespaces:
113
+ message_parts.append(f"\n\nAvailable namespaces: {', '.join(available_namespaces)}")
114
+
115
+ # Add helpful tip
116
+ message_parts.append(
117
+ "\n\nTip: Use `await registry.list_tools()` to see all registered tools, "
118
+ "or `await registry.list_namespaces()` to see available namespaces."
119
+ )
120
+
121
+ message = "".join(message_parts)
122
+
123
+ # Store details
124
+ details: dict[str, Any] = {"tool_name": tool_name, "namespace": namespace}
79
125
  if available_tools:
80
126
  details["available_tools"] = available_tools
127
+ if available_namespaces:
128
+ details["available_namespaces"] = available_namespaces
129
+ if similar_tools:
130
+ details["suggestions"] = similar_tools
131
+
81
132
  super().__init__(
82
- f"Tool '{tool_name}' not found in registry",
133
+ message,
83
134
  code=ErrorCode.TOOL_NOT_FOUND,
84
135
  details=details,
85
136
  )