krons 0.1.0__py3-none-any.whl → 0.2.0__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.
Files changed (162) hide show
  1. krons/__init__.py +49 -0
  2. krons/agent/__init__.py +144 -0
  3. krons/agent/mcps/__init__.py +14 -0
  4. krons/agent/mcps/loader.py +287 -0
  5. krons/agent/mcps/wrapper.py +799 -0
  6. krons/agent/message/__init__.py +20 -0
  7. krons/agent/message/action.py +69 -0
  8. krons/agent/message/assistant.py +52 -0
  9. krons/agent/message/common.py +49 -0
  10. krons/agent/message/instruction.py +130 -0
  11. krons/agent/message/prepare_msg.py +187 -0
  12. krons/agent/message/role.py +53 -0
  13. krons/agent/message/system.py +53 -0
  14. krons/agent/operations/__init__.py +82 -0
  15. krons/agent/operations/act.py +100 -0
  16. krons/agent/operations/generate.py +145 -0
  17. krons/agent/operations/llm_reparse.py +89 -0
  18. krons/agent/operations/operate.py +247 -0
  19. krons/agent/operations/parse.py +243 -0
  20. krons/agent/operations/react.py +286 -0
  21. krons/agent/operations/specs.py +235 -0
  22. krons/agent/operations/structure.py +151 -0
  23. krons/agent/operations/utils.py +79 -0
  24. krons/agent/providers/__init__.py +17 -0
  25. krons/agent/providers/anthropic_messages.py +146 -0
  26. krons/agent/providers/claude_code.py +276 -0
  27. krons/agent/providers/gemini.py +268 -0
  28. krons/agent/providers/match.py +75 -0
  29. krons/agent/providers/oai_chat.py +174 -0
  30. krons/agent/third_party/__init__.py +2 -0
  31. krons/agent/third_party/anthropic_models.py +154 -0
  32. krons/agent/third_party/claude_code.py +682 -0
  33. krons/agent/third_party/gemini_models.py +508 -0
  34. krons/agent/third_party/openai_models.py +295 -0
  35. krons/agent/tool.py +291 -0
  36. krons/core/__init__.py +127 -0
  37. krons/core/base/__init__.py +121 -0
  38. {kronos/core → krons/core/base}/broadcaster.py +7 -3
  39. {kronos/core → krons/core/base}/element.py +15 -7
  40. {kronos/core → krons/core/base}/event.py +41 -8
  41. {kronos/core → krons/core/base}/eventbus.py +4 -2
  42. {kronos/core → krons/core/base}/flow.py +14 -7
  43. {kronos/core → krons/core/base}/graph.py +27 -11
  44. {kronos/core → krons/core/base}/node.py +47 -22
  45. {kronos/core → krons/core/base}/pile.py +26 -12
  46. {kronos/core → krons/core/base}/processor.py +23 -9
  47. {kronos/core → krons/core/base}/progression.py +5 -3
  48. {kronos → krons/core}/specs/__init__.py +0 -5
  49. {kronos → krons/core}/specs/adapters/dataclass_field.py +16 -8
  50. {kronos → krons/core}/specs/adapters/pydantic_adapter.py +11 -5
  51. {kronos → krons/core}/specs/adapters/sql_ddl.py +16 -10
  52. {kronos → krons/core}/specs/catalog/__init__.py +2 -2
  53. {kronos → krons/core}/specs/catalog/_audit.py +3 -3
  54. {kronos → krons/core}/specs/catalog/_common.py +2 -2
  55. {kronos → krons/core}/specs/catalog/_content.py +5 -5
  56. {kronos → krons/core}/specs/catalog/_enforcement.py +4 -4
  57. {kronos → krons/core}/specs/factory.py +7 -7
  58. {kronos → krons/core}/specs/operable.py +9 -3
  59. {kronos → krons/core}/specs/protocol.py +4 -2
  60. {kronos → krons/core}/specs/spec.py +25 -13
  61. {kronos → krons/core}/types/base.py +7 -5
  62. {kronos → krons/core}/types/db_types.py +2 -2
  63. {kronos → krons/core}/types/identity.py +1 -1
  64. {kronos → krons}/errors.py +13 -13
  65. {kronos → krons}/protocols.py +9 -4
  66. krons/resource/__init__.py +89 -0
  67. {kronos/services → krons/resource}/backend.py +50 -24
  68. {kronos/services → krons/resource}/endpoint.py +28 -14
  69. {kronos/services → krons/resource}/hook.py +22 -9
  70. {kronos/services → krons/resource}/imodel.py +50 -32
  71. {kronos/services → krons/resource}/registry.py +27 -25
  72. {kronos/services → krons/resource}/utilities/rate_limited_executor.py +10 -6
  73. {kronos/services → krons/resource}/utilities/rate_limiter.py +4 -2
  74. {kronos/services → krons/resource}/utilities/resilience.py +17 -7
  75. krons/resource/utilities/token_calculator.py +185 -0
  76. {kronos → krons}/session/__init__.py +12 -17
  77. krons/session/constraints.py +70 -0
  78. {kronos → krons}/session/exchange.py +14 -6
  79. {kronos → krons}/session/message.py +4 -2
  80. krons/session/registry.py +35 -0
  81. {kronos → krons}/session/session.py +165 -174
  82. krons/utils/__init__.py +85 -0
  83. krons/utils/_function_arg_parser.py +99 -0
  84. krons/utils/_pythonic_function_call.py +249 -0
  85. {kronos → krons}/utils/_to_list.py +9 -3
  86. {kronos → krons}/utils/_utils.py +9 -5
  87. {kronos → krons}/utils/concurrency/__init__.py +38 -38
  88. {kronos → krons}/utils/concurrency/_async_call.py +6 -4
  89. {kronos → krons}/utils/concurrency/_errors.py +3 -1
  90. {kronos → krons}/utils/concurrency/_patterns.py +3 -1
  91. {kronos → krons}/utils/concurrency/_resource_tracker.py +6 -2
  92. krons/utils/display.py +257 -0
  93. {kronos → krons}/utils/fuzzy/__init__.py +6 -1
  94. {kronos → krons}/utils/fuzzy/_fuzzy_match.py +14 -8
  95. {kronos → krons}/utils/fuzzy/_string_similarity.py +3 -1
  96. {kronos → krons}/utils/fuzzy/_to_dict.py +3 -1
  97. krons/utils/schemas/__init__.py +26 -0
  98. krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
  99. krons/utils/schemas/_formatter.py +72 -0
  100. krons/utils/schemas/_minimal_yaml.py +151 -0
  101. krons/utils/schemas/_typescript.py +153 -0
  102. {kronos → krons}/utils/sql/_sql_validation.py +1 -1
  103. krons/utils/validators/__init__.py +3 -0
  104. krons/utils/validators/_validate_image_url.py +56 -0
  105. krons/work/__init__.py +126 -0
  106. krons/work/engine.py +333 -0
  107. krons/work/form.py +305 -0
  108. {kronos → krons/work}/operations/__init__.py +7 -4
  109. {kronos → krons/work}/operations/builder.py +4 -4
  110. {kronos/enforcement → krons/work/operations}/context.py +37 -6
  111. {kronos → krons/work}/operations/flow.py +17 -9
  112. krons/work/operations/node.py +103 -0
  113. krons/work/operations/registry.py +103 -0
  114. {kronos/specs → krons/work}/phrase.py +131 -14
  115. {kronos/enforcement → krons/work}/policy.py +3 -3
  116. krons/work/report.py +268 -0
  117. krons/work/rules/__init__.py +47 -0
  118. {kronos/enforcement → krons/work/rules}/common/boolean.py +3 -1
  119. {kronos/enforcement → krons/work/rules}/common/choice.py +9 -3
  120. {kronos/enforcement → krons/work/rules}/common/number.py +3 -1
  121. {kronos/enforcement → krons/work/rules}/common/string.py +9 -3
  122. {kronos/enforcement → krons/work/rules}/rule.py +2 -2
  123. {kronos/enforcement → krons/work/rules}/validator.py +21 -6
  124. {kronos/enforcement → krons/work}/service.py +16 -7
  125. krons/work/worker.py +266 -0
  126. {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/METADATA +19 -5
  127. krons-0.2.0.dist-info/RECORD +154 -0
  128. kronos/core/__init__.py +0 -145
  129. kronos/enforcement/__init__.py +0 -57
  130. kronos/operations/node.py +0 -101
  131. kronos/operations/registry.py +0 -92
  132. kronos/services/__init__.py +0 -81
  133. kronos/specs/adapters/__init__.py +0 -0
  134. kronos/utils/__init__.py +0 -40
  135. krons-0.1.0.dist-info/RECORD +0 -101
  136. {kronos → krons/core/specs/adapters}/__init__.py +0 -0
  137. {kronos → krons/core}/specs/adapters/_utils.py +0 -0
  138. {kronos → krons/core}/specs/adapters/factory.py +0 -0
  139. {kronos → krons/core}/types/__init__.py +0 -0
  140. {kronos → krons/core}/types/_sentinel.py +0 -0
  141. {kronos → krons}/py.typed +0 -0
  142. {kronos/services → krons/resource}/utilities/__init__.py +0 -0
  143. {kronos/services → krons/resource}/utilities/header_factory.py +0 -0
  144. {kronos → krons}/utils/_hash.py +0 -0
  145. {kronos → krons}/utils/_json_dump.py +0 -0
  146. {kronos → krons}/utils/_lazy_init.py +0 -0
  147. {kronos → krons}/utils/_to_num.py +0 -0
  148. {kronos → krons}/utils/concurrency/_cancel.py +0 -0
  149. {kronos → krons}/utils/concurrency/_primitives.py +0 -0
  150. {kronos → krons}/utils/concurrency/_priority_queue.py +0 -0
  151. {kronos → krons}/utils/concurrency/_run_async.py +0 -0
  152. {kronos → krons}/utils/concurrency/_task.py +0 -0
  153. {kronos → krons}/utils/concurrency/_utils.py +0 -0
  154. {kronos → krons}/utils/fuzzy/_extract_json.py +0 -0
  155. {kronos → krons}/utils/fuzzy/_fuzzy_json.py +0 -0
  156. {kronos → krons}/utils/sql/__init__.py +0 -0
  157. {kronos/enforcement → krons/work/rules}/common/__init__.py +0 -0
  158. {kronos/enforcement → krons/work/rules}/common/mapping.py +0 -0
  159. {kronos/enforcement → krons/work/rules}/common/model.py +0 -0
  160. {kronos/enforcement → krons/work/rules}/registry.py +0 -0
  161. {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/WHEEL +0 -0
  162. {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/licenses/LICENSE +0 -0
krons/__init__.py ADDED
@@ -0,0 +1,49 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import TYPE_CHECKING
7
+
8
+ # Lazy module re-exports via __getattr__
9
+ _MODULE_ALIASES: dict[str, str] = {
10
+ "types": "krons.core.types",
11
+ "specs": "krons.core.specs",
12
+ "session": "krons.session",
13
+ "operations": "krons.work.operations",
14
+ "agent": "krons.agent",
15
+ "resource": "krons.resource",
16
+ "work": "krons.work",
17
+ }
18
+
19
+ _LOADED_MODULES: dict[str, object] = {}
20
+
21
+
22
+ def __getattr__(name: str) -> object:
23
+ """Lazy load aliased modules."""
24
+ if name in _LOADED_MODULES:
25
+ return _LOADED_MODULES[name]
26
+
27
+ if name in _MODULE_ALIASES:
28
+ from importlib import import_module
29
+
30
+ module = import_module(_MODULE_ALIASES[name])
31
+ _LOADED_MODULES[name] = module
32
+ return module
33
+
34
+ raise AttributeError(f"module 'krons' has no attribute {name!r}")
35
+
36
+
37
+ def __dir__() -> list[str]:
38
+ """List available attributes."""
39
+ return list(_MODULE_ALIASES.keys())
40
+
41
+
42
+ if TYPE_CHECKING:
43
+ from krons import agent as agent
44
+ from krons import resource as resource
45
+ from krons import session as session
46
+ from krons import work as work
47
+ from krons.core import specs as specs
48
+ from krons.core import types as types
49
+ from krons.work import operations as operations
@@ -0,0 +1,144 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Agents module - AI provider integrations, message handling, and operations.
5
+
6
+ Submodules:
7
+ providers: API endpoint implementations (OpenAI, Anthropic, Gemini, Claude Code)
8
+ message: Message content types and preparation
9
+ mcps: MCP (Model Context Protocol) connection management
10
+ operations: Agent operation primitives (generate, parse, act, react)
11
+ third_party: Provider-specific request/response models
12
+ tool: Callable function backends
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import TYPE_CHECKING
18
+
19
+ # Lazy import mapping
20
+ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
21
+ # tool.py
22
+ "Tool": ("krons.agent.tool", "Tool"),
23
+ "ToolCalling": ("krons.agent.tool", "ToolCalling"),
24
+ "ToolConfig": ("krons.agent.tool", "ToolConfig"),
25
+ "tool": ("krons.agent.tool", "tool"),
26
+ # providers
27
+ "AnthropicMessagesEndpoint": (
28
+ "krons.agent.providers",
29
+ "AnthropicMessagesEndpoint",
30
+ ),
31
+ "GeminiCodeEndpoint": ("krons.agent.providers", "GeminiCodeEndpoint"),
32
+ "OAIChatEndpoint": ("krons.agent.providers", "OAIChatEndpoint"),
33
+ "create_anthropic_config": ("krons.agent.providers", "create_anthropic_config"),
34
+ "create_gemini_code_config": (
35
+ "krons.agent.providers",
36
+ "create_gemini_code_config",
37
+ ),
38
+ "create_oai_chat": ("krons.agent.providers", "create_oai_chat"),
39
+ "match_endpoint": ("krons.agent.providers", "match_endpoint"),
40
+ # message
41
+ "ActionRequest": ("krons.agent.message", "ActionRequest"),
42
+ "ActionResponse": ("krons.agent.message", "ActionResponse"),
43
+ "Assistant": ("krons.agent.message", "Assistant"),
44
+ "Instruction": ("krons.agent.message", "Instruction"),
45
+ "MessageContent": ("krons.agent.message", "MessageContent"),
46
+ "MessageRole": ("krons.agent.message", "MessageRole"),
47
+ "System": ("krons.agent.message", "System"),
48
+ "prepare_messages_for_chat": ("krons.agent.message", "prepare_messages_for_chat"),
49
+ # mcps
50
+ "CommandNotAllowedError": ("krons.agent.mcps", "CommandNotAllowedError"),
51
+ "DEFAULT_ALLOWED_COMMANDS": ("krons.agent.mcps", "DEFAULT_ALLOWED_COMMANDS"),
52
+ "MCPConnectionPool": ("krons.agent.mcps", "MCPConnectionPool"),
53
+ "create_mcp_callable": ("krons.agent.mcps", "create_mcp_callable"),
54
+ "load_mcp_config": ("krons.agent.mcps", "load_mcp_config"),
55
+ "load_mcp_tools": ("krons.agent.mcps", "load_mcp_tools"),
56
+ }
57
+
58
+ _LOADED: dict[str, object] = {}
59
+
60
+
61
+ def __getattr__(name: str) -> object:
62
+ """Lazy import attributes on first access."""
63
+ if name in _LOADED:
64
+ return _LOADED[name]
65
+
66
+ if name in _LAZY_IMPORTS:
67
+ from importlib import import_module
68
+
69
+ module_name, attr_name = _LAZY_IMPORTS[name]
70
+ module = import_module(module_name)
71
+ value = getattr(module, attr_name)
72
+ _LOADED[name] = value
73
+ return value
74
+
75
+ raise AttributeError(f"module 'krons.agent' has no attribute {name!r}")
76
+
77
+
78
+ def __dir__() -> list[str]:
79
+ """Return all available attributes for autocomplete."""
80
+ return list(__all__)
81
+
82
+
83
+ # TYPE_CHECKING block for static analysis
84
+ if TYPE_CHECKING:
85
+ from krons.agent.mcps import (
86
+ DEFAULT_ALLOWED_COMMANDS,
87
+ CommandNotAllowedError,
88
+ MCPConnectionPool,
89
+ create_mcp_callable,
90
+ load_mcp_config,
91
+ load_mcp_tools,
92
+ )
93
+ from krons.agent.message import (
94
+ ActionRequest,
95
+ ActionResponse,
96
+ Assistant,
97
+ Instruction,
98
+ MessageContent,
99
+ MessageRole,
100
+ System,
101
+ prepare_messages_for_chat,
102
+ )
103
+ from krons.agent.providers import (
104
+ AnthropicMessagesEndpoint,
105
+ GeminiCodeEndpoint,
106
+ OAIChatEndpoint,
107
+ create_anthropic_config,
108
+ create_gemini_code_config,
109
+ create_oai_chat,
110
+ match_endpoint,
111
+ )
112
+ from krons.agent.tool import Tool, ToolCalling, ToolConfig, tool
113
+
114
+ __all__ = [
115
+ # tool
116
+ "Tool",
117
+ "ToolCalling",
118
+ "ToolConfig",
119
+ "tool",
120
+ # providers
121
+ "AnthropicMessagesEndpoint",
122
+ "GeminiCodeEndpoint",
123
+ "OAIChatEndpoint",
124
+ "create_anthropic_config",
125
+ "create_gemini_code_config",
126
+ "create_oai_chat",
127
+ "match_endpoint",
128
+ # message
129
+ "ActionRequest",
130
+ "ActionResponse",
131
+ "Assistant",
132
+ "Instruction",
133
+ "MessageContent",
134
+ "MessageRole",
135
+ "System",
136
+ "prepare_messages_for_chat",
137
+ # mcps
138
+ "CommandNotAllowedError",
139
+ "DEFAULT_ALLOWED_COMMANDS",
140
+ "MCPConnectionPool",
141
+ "create_mcp_callable",
142
+ "load_mcp_config",
143
+ "load_mcp_tools",
144
+ ]
@@ -0,0 +1,14 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from .loader import create_mcp_callable, load_mcp_config, load_mcp_tools
5
+ from .wrapper import DEFAULT_ALLOWED_COMMANDS, CommandNotAllowedError, MCPConnectionPool
6
+
7
+ __all__ = (
8
+ "DEFAULT_ALLOWED_COMMANDS",
9
+ "CommandNotAllowedError",
10
+ "MCPConnectionPool",
11
+ "create_mcp_callable",
12
+ "load_mcp_config",
13
+ "load_mcp_tools",
14
+ )
@@ -0,0 +1,287 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ from collections.abc import Callable
8
+ from typing import Any
9
+
10
+ __all__ = ("create_mcp_callable", "load_mcp_config", "load_mcp_tools")
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ async def load_mcp_tools(
16
+ registry: Any,
17
+ server_config: dict[str, Any],
18
+ tool_names: list[str] | None = None,
19
+ request_options: dict[str, type] | None = None,
20
+ update: bool = False,
21
+ ) -> list[str]:
22
+ """Load MCP tools into ResourceRegistry.
23
+
24
+ Converts MCP tools to standard Tool instances wrapped in iModel
25
+ and registers them in the provided ResourceRegistry.
26
+
27
+ Args:
28
+ registry: ResourceRegistry instance to register tools into
29
+ server_config: MCP server configuration (command, args, etc.)
30
+ Can be {"server": "name"} to reference loaded config
31
+ or full config dict with command/args
32
+ tool_names: Optional list of specific tool names to register.
33
+ If None, will discover and register all available tools.
34
+ request_options: Optional dict mapping tool names to Pydantic model classes
35
+ for request validation. E.g., {"exa_search": ExaSearchRequest}
36
+ update: If True, allow updating existing tools.
37
+
38
+ Returns:
39
+ List of registered tool names (qualified with server prefix)
40
+
41
+ Example:
42
+ >>> # Auto-discover all tools
43
+ >>> tools = await load_mcp_tools(session.registry, {"server": "search"})
44
+ >>>
45
+ >>> # Register specific tools with validation
46
+ >>> from pydantic import BaseModel
47
+ >>> class SearchRequest(BaseModel):
48
+ ... query: str
49
+ ... limit: int = 10
50
+ >>> tools = await load_mcp_tools(
51
+ ... session.registry,
52
+ ... {"command": "python", "args": ["-m", "server"]},
53
+ ... tool_names=["search"],
54
+ ... request_options={"search": SearchRequest},
55
+ ... )
56
+ """
57
+ from krons.agent.tool import Tool, ToolConfig
58
+ from krons.resource import iModel
59
+
60
+ from .wrapper import MCPConnectionPool
61
+
62
+ registered_tools = []
63
+
64
+ # Extract server name for qualified naming
65
+ server_name = None
66
+ if isinstance(server_config, dict) and "server" in server_config:
67
+ server_name = server_config["server"]
68
+
69
+ if tool_names:
70
+ # Register specific tools
71
+ for tool_name in tool_names:
72
+ # Qualified name to avoid collisions
73
+ qualified_name = f"{server_name}_{tool_name}" if server_name else tool_name
74
+
75
+ # Check for existing registration
76
+ if registry.has(qualified_name) and not update:
77
+ raise ValueError(
78
+ f"Tool '{qualified_name}' already registered. Use update=True to replace."
79
+ )
80
+
81
+ # Get request_options for this tool if provided
82
+ tool_request_options = None
83
+ if request_options and tool_name in request_options:
84
+ tool_request_options = request_options[tool_name]
85
+
86
+ # Create MCP wrapper callable
87
+ mcp_callable = create_mcp_callable(
88
+ server_config=server_config,
89
+ tool_name=tool_name,
90
+ )
91
+
92
+ # Create and register Tool
93
+ try:
94
+ tool = Tool(
95
+ func_callable=mcp_callable,
96
+ config=ToolConfig(
97
+ name=qualified_name,
98
+ provider=server_name or "mcp",
99
+ request_options=tool_request_options,
100
+ ),
101
+ )
102
+ model = iModel(backend=tool)
103
+ registry.register(model, update=update)
104
+ registered_tools.append(qualified_name)
105
+ except Exception as e:
106
+ logger.warning(f"Failed to register tool {tool_name}: {e}")
107
+ else:
108
+ # Auto-discover tools from the server
109
+ client = await MCPConnectionPool.get_client(server_config)
110
+ tools = await client.list_tools()
111
+
112
+ # Register each discovered tool
113
+ for tool in tools:
114
+ # Qualified name to avoid collisions
115
+ qualified_name = f"{server_name}_{tool.name}" if server_name else tool.name
116
+
117
+ # Get request_options for this tool if provided
118
+ tool_request_options = None
119
+ if request_options and tool.name in request_options:
120
+ tool_request_options = request_options[tool.name]
121
+
122
+ # Extract schema from FastMCP tool directly
123
+ tool_schema = None
124
+ try:
125
+ if (
126
+ hasattr(tool, "inputSchema")
127
+ and tool.inputSchema is not None
128
+ and isinstance(tool.inputSchema, dict)
129
+ ):
130
+ # Format as OpenAI function calling schema
131
+ tool_schema = {
132
+ "type": "function",
133
+ "function": {
134
+ "name": qualified_name, # Use qualified name
135
+ "description": (
136
+ tool.description
137
+ if hasattr(tool, "description") and tool.description
138
+ else f"MCP tool: {tool.name}"
139
+ ),
140
+ "parameters": tool.inputSchema,
141
+ },
142
+ }
143
+ except Exception as schema_error:
144
+ # If schema extraction fails, Tool will auto-generate from callable signature
145
+ logger.warning(
146
+ f"Could not extract schema for {tool.name}: {schema_error}"
147
+ )
148
+ tool_schema = None
149
+
150
+ try:
151
+ # Create MCP wrapper callable
152
+ mcp_callable = create_mcp_callable(
153
+ server_config=server_config,
154
+ tool_name=tool.name,
155
+ )
156
+
157
+ # Register as regular Tool with MCP-discovered schema
158
+ if registry.has(qualified_name) and not update:
159
+ logger.warning(
160
+ f"Tool '{qualified_name}' already registered. Skipping."
161
+ )
162
+ continue
163
+
164
+ tool_obj = Tool(
165
+ func_callable=mcp_callable,
166
+ config=ToolConfig(
167
+ name=qualified_name,
168
+ provider=server_name or "mcp",
169
+ request_options=tool_request_options,
170
+ ),
171
+ tool_schema=tool_schema,
172
+ )
173
+ model = iModel(backend=tool_obj)
174
+ registry.register(model, update=update)
175
+ registered_tools.append(qualified_name)
176
+ except Exception as e:
177
+ logger.warning(f"Failed to register tool {tool.name}: {e}")
178
+
179
+ return registered_tools
180
+
181
+
182
+ def create_mcp_callable(
183
+ server_config: dict[str, Any],
184
+ tool_name: str,
185
+ ) -> Callable:
186
+ """Create async callable that wraps MCP tool execution.
187
+
188
+ Args:
189
+ server_config: MCP server configuration
190
+ tool_name: Original MCP tool name
191
+
192
+ Returns:
193
+ Async callable that executes MCP tool via connection pool
194
+ """
195
+ from .wrapper import MCPConnectionPool
196
+
197
+ async def mcp_wrapper(**kwargs: Any) -> Any:
198
+ """Execute MCP tool call via connection pool.
199
+
200
+ Args:
201
+ **kwargs: Tool arguments
202
+
203
+ Returns:
204
+ Tool execution result (normalized)
205
+ """
206
+ # Get pooled client
207
+ client = await MCPConnectionPool.get_client(server_config)
208
+
209
+ # Call the tool
210
+ result = await client.call_tool(tool_name, kwargs)
211
+
212
+ # Extract content from FastMCP response
213
+ if hasattr(result, "content"):
214
+ content = result.content
215
+ if isinstance(content, list) and len(content) == 1:
216
+ item = content[0]
217
+ if hasattr(item, "text"):
218
+ return item.text
219
+ elif isinstance(item, dict) and item.get("type") == "text":
220
+ return item.get("text", "")
221
+ return content
222
+ elif isinstance(result, list) and len(result) == 1:
223
+ item = result[0]
224
+ if isinstance(item, dict) and item.get("type") == "text":
225
+ return item.get("text", "")
226
+
227
+ return result
228
+
229
+ # Set function name for introspection
230
+ mcp_wrapper.__name__ = tool_name
231
+
232
+ return mcp_wrapper
233
+
234
+
235
+ async def load_mcp_config(
236
+ registry: Any, # ResourceRegistry
237
+ config_path: str,
238
+ server_names: list[str] | None = None,
239
+ update: bool = False,
240
+ ) -> dict[str, list[str]]:
241
+ """Load MCP configurations from a .mcp.json file with auto-discovery.
242
+
243
+ Args:
244
+ registry: ResourceRegistry instance to register tools into
245
+ config_path: Path to .mcp.json configuration file
246
+ server_names: Optional list of server names to load.
247
+ If None, loads all servers.
248
+ update: If True, allow updating existing tools.
249
+
250
+ Returns:
251
+ Dict mapping server names to lists of registered tool names
252
+
253
+ Example:
254
+ >>> # Load all servers and auto-discover their tools
255
+ >>> tools = await load_mcp_config(session.registry, ".mcp.json")
256
+ >>> print(f"Loaded {sum(len(t) for t in tools.values())} tools")
257
+ >>>
258
+ >>> # Load specific servers only
259
+ >>> tools = await load_mcp_config(
260
+ ... session.registry, ".mcp.json", server_names=["search", "memory"]
261
+ ... )
262
+ """
263
+ from .wrapper import MCPConnectionPool
264
+
265
+ # Load the config file into the connection pool
266
+ MCPConnectionPool.load_config(config_path)
267
+
268
+ # Get server list to process
269
+ if server_names is None:
270
+ # Get all server names from loaded config
271
+ server_names = list(MCPConnectionPool._configs.keys())
272
+
273
+ # Register tools from each server
274
+ all_tools = {}
275
+ for server_name in server_names:
276
+ try:
277
+ # Register using server reference
278
+ tools = await load_mcp_tools(
279
+ registry, {"server": server_name}, update=update
280
+ )
281
+ all_tools[server_name] = tools
282
+ logger.info(f"✅ Registered {len(tools)} tools from server '{server_name}'")
283
+ except Exception as e:
284
+ logger.error(f"⚠️ Failed to register server '{server_name}': {e}")
285
+ all_tools[server_name] = []
286
+
287
+ return all_tools