krons 0.1.1__py3-none-any.whl → 0.2.1__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.
- krons/__init__.py +49 -0
- krons/agent/__init__.py +144 -0
- krons/agent/mcps/__init__.py +14 -0
- krons/agent/mcps/loader.py +287 -0
- krons/agent/mcps/wrapper.py +799 -0
- krons/agent/message/__init__.py +20 -0
- krons/agent/message/action.py +69 -0
- krons/agent/message/assistant.py +52 -0
- krons/agent/message/common.py +49 -0
- krons/agent/message/instruction.py +130 -0
- krons/agent/message/prepare_msg.py +187 -0
- krons/agent/message/role.py +53 -0
- krons/agent/message/system.py +53 -0
- krons/agent/operations/__init__.py +82 -0
- krons/agent/operations/act.py +100 -0
- krons/agent/operations/generate.py +145 -0
- krons/agent/operations/llm_reparse.py +89 -0
- krons/agent/operations/operate.py +247 -0
- krons/agent/operations/parse.py +243 -0
- krons/agent/operations/react.py +286 -0
- krons/agent/operations/specs.py +235 -0
- krons/agent/operations/structure.py +151 -0
- krons/agent/operations/utils.py +79 -0
- krons/agent/providers/__init__.py +17 -0
- krons/agent/providers/anthropic_messages.py +146 -0
- krons/agent/providers/claude_code.py +276 -0
- krons/agent/providers/gemini.py +268 -0
- krons/agent/providers/match.py +75 -0
- krons/agent/providers/oai_chat.py +174 -0
- krons/agent/third_party/__init__.py +2 -0
- krons/agent/third_party/anthropic_models.py +154 -0
- krons/agent/third_party/claude_code.py +682 -0
- krons/agent/third_party/gemini_models.py +508 -0
- krons/agent/third_party/openai_models.py +295 -0
- krons/agent/tool.py +291 -0
- krons/core/__init__.py +56 -74
- krons/core/base/__init__.py +121 -0
- krons/core/{broadcaster.py → base/broadcaster.py} +7 -3
- krons/core/{element.py → base/element.py} +13 -5
- krons/core/{event.py → base/event.py} +39 -6
- krons/core/{eventbus.py → base/eventbus.py} +3 -1
- krons/core/{flow.py → base/flow.py} +11 -4
- krons/core/{graph.py → base/graph.py} +24 -8
- krons/core/{node.py → base/node.py} +44 -19
- krons/core/{pile.py → base/pile.py} +22 -8
- krons/core/{processor.py → base/processor.py} +21 -7
- krons/core/{progression.py → base/progression.py} +3 -1
- krons/{specs → core/specs}/__init__.py +0 -5
- krons/{specs → core/specs}/adapters/dataclass_field.py +16 -8
- krons/{specs → core/specs}/adapters/pydantic_adapter.py +11 -5
- krons/{specs → core/specs}/adapters/sql_ddl.py +14 -8
- krons/{specs → core/specs}/catalog/__init__.py +2 -2
- krons/{specs → core/specs}/catalog/_audit.py +2 -2
- krons/{specs → core/specs}/catalog/_common.py +2 -2
- krons/{specs → core/specs}/catalog/_content.py +4 -4
- krons/{specs → core/specs}/catalog/_enforcement.py +3 -3
- krons/{specs → core/specs}/factory.py +5 -5
- krons/{specs → core/specs}/operable.py +8 -2
- krons/{specs → core/specs}/protocol.py +4 -2
- krons/{specs → core/specs}/spec.py +23 -11
- krons/{types → core/types}/base.py +4 -2
- krons/{types → core/types}/db_types.py +2 -2
- krons/errors.py +13 -13
- krons/protocols.py +9 -4
- krons/resource/__init__.py +89 -0
- krons/{services → resource}/backend.py +48 -22
- krons/{services → resource}/endpoint.py +28 -14
- krons/{services → resource}/hook.py +20 -7
- krons/{services → resource}/imodel.py +46 -28
- krons/{services → resource}/registry.py +26 -24
- krons/{services → resource}/utilities/rate_limited_executor.py +7 -3
- krons/{services → resource}/utilities/rate_limiter.py +3 -1
- krons/{services → resource}/utilities/resilience.py +15 -5
- krons/resource/utilities/token_calculator.py +185 -0
- krons/session/__init__.py +12 -17
- krons/session/constraints.py +70 -0
- krons/session/exchange.py +11 -3
- krons/session/message.py +3 -1
- krons/session/registry.py +35 -0
- krons/session/session.py +165 -174
- krons/utils/__init__.py +45 -0
- krons/utils/_function_arg_parser.py +99 -0
- krons/utils/_pythonic_function_call.py +249 -0
- krons/utils/_to_list.py +9 -3
- krons/utils/_utils.py +6 -2
- krons/utils/concurrency/_async_call.py +4 -2
- krons/utils/concurrency/_errors.py +3 -1
- krons/utils/concurrency/_patterns.py +3 -1
- krons/utils/concurrency/_resource_tracker.py +6 -2
- krons/utils/display.py +257 -0
- krons/utils/fuzzy/__init__.py +6 -1
- krons/utils/fuzzy/_fuzzy_match.py +14 -8
- krons/utils/fuzzy/_string_similarity.py +3 -1
- krons/utils/fuzzy/_to_dict.py +3 -1
- krons/utils/schemas/__init__.py +26 -0
- krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
- krons/utils/schemas/_formatter.py +72 -0
- krons/utils/schemas/_minimal_yaml.py +151 -0
- krons/utils/schemas/_typescript.py +153 -0
- krons/utils/validators/__init__.py +3 -0
- krons/utils/validators/_validate_image_url.py +56 -0
- krons/work/__init__.py +115 -0
- krons/work/engine.py +333 -0
- krons/work/form.py +242 -0
- krons/{operations → work/operations}/__init__.py +7 -4
- krons/{operations → work/operations}/builder.py +1 -1
- krons/{enforcement → work/operations}/context.py +36 -5
- krons/{operations → work/operations}/flow.py +13 -5
- krons/{operations → work/operations}/node.py +45 -43
- krons/work/operations/registry.py +103 -0
- krons/work/report.py +268 -0
- krons/work/rules/__init__.py +47 -0
- krons/{enforcement → work/rules}/common/boolean.py +3 -1
- krons/{enforcement → work/rules}/common/choice.py +9 -3
- krons/{enforcement → work/rules}/common/number.py +3 -1
- krons/{enforcement → work/rules}/common/string.py +9 -3
- krons/{enforcement → work/rules}/rule.py +1 -1
- krons/{enforcement → work/rules}/validator.py +20 -5
- krons/work/worker.py +266 -0
- {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/METADATA +15 -1
- krons-0.2.1.dist-info/RECORD +151 -0
- krons/enforcement/__init__.py +0 -57
- krons/enforcement/policy.py +0 -80
- krons/enforcement/service.py +0 -370
- krons/operations/registry.py +0 -92
- krons/services/__init__.py +0 -81
- krons/specs/phrase.py +0 -405
- krons-0.1.1.dist-info/RECORD +0 -101
- /krons/{specs → core/specs}/adapters/__init__.py +0 -0
- /krons/{specs → core/specs}/adapters/_utils.py +0 -0
- /krons/{specs → core/specs}/adapters/factory.py +0 -0
- /krons/{types → core/types}/__init__.py +0 -0
- /krons/{types → core/types}/_sentinel.py +0 -0
- /krons/{types → core/types}/identity.py +0 -0
- /krons/{services → resource}/utilities/__init__.py +0 -0
- /krons/{services → resource}/utilities/header_factory.py +0 -0
- /krons/{enforcement → work/rules}/common/__init__.py +0 -0
- /krons/{enforcement → work/rules}/common/mapping.py +0 -0
- /krons/{enforcement → work/rules}/common/model.py +0 -0
- /krons/{enforcement → work/rules}/registry.py +0 -0
- {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/WHEEL +0 -0
- {krons-0.1.1.dist-info → krons-0.2.1.dist-info}/licenses/LICENSE +0 -0
krons/__init__.py
CHANGED
|
@@ -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
|
krons/agent/__init__.py
ADDED
|
@@ -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
|