hammad-python 0.0.30__py3-none-any.whl → 0.0.32__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.
- ham/__init__.py +200 -0
- {hammad_python-0.0.30.dist-info → hammad_python-0.0.32.dist-info}/METADATA +6 -32
- hammad_python-0.0.32.dist-info/RECORD +6 -0
- hammad/__init__.py +0 -84
- hammad/_internal.py +0 -256
- hammad/_main.py +0 -226
- hammad/cache/__init__.py +0 -40
- hammad/cache/base_cache.py +0 -181
- hammad/cache/cache.py +0 -169
- hammad/cache/decorators.py +0 -261
- hammad/cache/file_cache.py +0 -80
- hammad/cache/ttl_cache.py +0 -74
- hammad/cli/__init__.py +0 -33
- hammad/cli/animations.py +0 -573
- hammad/cli/plugins.py +0 -867
- hammad/cli/styles/__init__.py +0 -55
- hammad/cli/styles/settings.py +0 -139
- hammad/cli/styles/types.py +0 -358
- hammad/cli/styles/utils.py +0 -634
- hammad/data/__init__.py +0 -90
- hammad/data/collections/__init__.py +0 -49
- hammad/data/collections/collection.py +0 -326
- hammad/data/collections/indexes/__init__.py +0 -37
- hammad/data/collections/indexes/qdrant/__init__.py +0 -1
- hammad/data/collections/indexes/qdrant/index.py +0 -723
- hammad/data/collections/indexes/qdrant/settings.py +0 -94
- hammad/data/collections/indexes/qdrant/utils.py +0 -210
- hammad/data/collections/indexes/tantivy/__init__.py +0 -1
- hammad/data/collections/indexes/tantivy/index.py +0 -426
- hammad/data/collections/indexes/tantivy/settings.py +0 -40
- hammad/data/collections/indexes/tantivy/utils.py +0 -176
- hammad/data/configurations/__init__.py +0 -35
- hammad/data/configurations/configuration.py +0 -564
- hammad/data/models/__init__.py +0 -50
- hammad/data/models/extensions/__init__.py +0 -4
- hammad/data/models/extensions/pydantic/__init__.py +0 -42
- hammad/data/models/extensions/pydantic/converters.py +0 -759
- hammad/data/models/fields.py +0 -546
- hammad/data/models/model.py +0 -1078
- hammad/data/models/utils.py +0 -280
- hammad/data/sql/__init__.py +0 -24
- hammad/data/sql/database.py +0 -576
- hammad/data/sql/types.py +0 -127
- hammad/data/types/__init__.py +0 -75
- hammad/data/types/file.py +0 -431
- hammad/data/types/multimodal/__init__.py +0 -36
- hammad/data/types/multimodal/audio.py +0 -200
- hammad/data/types/multimodal/image.py +0 -182
- hammad/data/types/text.py +0 -1308
- hammad/formatting/__init__.py +0 -33
- hammad/formatting/json/__init__.py +0 -27
- hammad/formatting/json/converters.py +0 -158
- hammad/formatting/text/__init__.py +0 -63
- hammad/formatting/text/converters.py +0 -723
- hammad/formatting/text/markdown.py +0 -131
- hammad/formatting/yaml/__init__.py +0 -26
- hammad/formatting/yaml/converters.py +0 -5
- hammad/genai/__init__.py +0 -217
- hammad/genai/a2a/__init__.py +0 -32
- hammad/genai/a2a/workers.py +0 -552
- hammad/genai/agents/__init__.py +0 -59
- hammad/genai/agents/agent.py +0 -1973
- hammad/genai/agents/run.py +0 -1024
- hammad/genai/agents/types/__init__.py +0 -42
- hammad/genai/agents/types/agent_context.py +0 -13
- hammad/genai/agents/types/agent_event.py +0 -128
- hammad/genai/agents/types/agent_hooks.py +0 -220
- hammad/genai/agents/types/agent_messages.py +0 -31
- hammad/genai/agents/types/agent_response.py +0 -125
- hammad/genai/agents/types/agent_stream.py +0 -327
- hammad/genai/graphs/__init__.py +0 -125
- hammad/genai/graphs/_utils.py +0 -190
- hammad/genai/graphs/base.py +0 -1828
- hammad/genai/graphs/plugins.py +0 -316
- hammad/genai/graphs/types.py +0 -638
- hammad/genai/models/__init__.py +0 -1
- hammad/genai/models/embeddings/__init__.py +0 -43
- hammad/genai/models/embeddings/model.py +0 -226
- hammad/genai/models/embeddings/run.py +0 -163
- hammad/genai/models/embeddings/types/__init__.py +0 -37
- hammad/genai/models/embeddings/types/embedding_model_name.py +0 -75
- hammad/genai/models/embeddings/types/embedding_model_response.py +0 -76
- hammad/genai/models/embeddings/types/embedding_model_run_params.py +0 -66
- hammad/genai/models/embeddings/types/embedding_model_settings.py +0 -47
- hammad/genai/models/language/__init__.py +0 -57
- hammad/genai/models/language/model.py +0 -1098
- hammad/genai/models/language/run.py +0 -878
- hammad/genai/models/language/types/__init__.py +0 -40
- hammad/genai/models/language/types/language_model_instructor_mode.py +0 -47
- hammad/genai/models/language/types/language_model_messages.py +0 -28
- hammad/genai/models/language/types/language_model_name.py +0 -239
- hammad/genai/models/language/types/language_model_request.py +0 -127
- hammad/genai/models/language/types/language_model_response.py +0 -217
- hammad/genai/models/language/types/language_model_response_chunk.py +0 -56
- hammad/genai/models/language/types/language_model_settings.py +0 -89
- hammad/genai/models/language/types/language_model_stream.py +0 -600
- hammad/genai/models/language/utils/__init__.py +0 -28
- hammad/genai/models/language/utils/requests.py +0 -421
- hammad/genai/models/language/utils/structured_outputs.py +0 -135
- hammad/genai/models/model_provider.py +0 -4
- hammad/genai/models/multimodal.py +0 -47
- hammad/genai/models/reranking.py +0 -26
- hammad/genai/types/__init__.py +0 -1
- hammad/genai/types/base.py +0 -215
- hammad/genai/types/history.py +0 -290
- hammad/genai/types/tools.py +0 -507
- hammad/logging/__init__.py +0 -35
- hammad/logging/decorators.py +0 -834
- hammad/logging/logger.py +0 -1018
- hammad/mcp/__init__.py +0 -53
- hammad/mcp/client/__init__.py +0 -35
- hammad/mcp/client/client.py +0 -624
- hammad/mcp/client/client_service.py +0 -400
- hammad/mcp/client/settings.py +0 -178
- hammad/mcp/servers/__init__.py +0 -26
- hammad/mcp/servers/launcher.py +0 -1161
- hammad/runtime/__init__.py +0 -32
- hammad/runtime/decorators.py +0 -142
- hammad/runtime/run.py +0 -299
- hammad/service/__init__.py +0 -49
- hammad/service/create.py +0 -527
- hammad/service/decorators.py +0 -283
- hammad/types.py +0 -288
- hammad/typing/__init__.py +0 -435
- hammad/web/__init__.py +0 -43
- hammad/web/http/__init__.py +0 -1
- hammad/web/http/client.py +0 -944
- hammad/web/models.py +0 -275
- hammad/web/openapi/__init__.py +0 -1
- hammad/web/openapi/client.py +0 -740
- hammad/web/search/__init__.py +0 -1
- hammad/web/search/client.py +0 -1023
- hammad/web/utils.py +0 -472
- hammad_python-0.0.30.dist-info/RECORD +0 -135
- {hammad → ham}/py.typed +0 -0
- {hammad_python-0.0.30.dist-info → hammad_python-0.0.32.dist-info}/WHEEL +0 -0
- {hammad_python-0.0.30.dist-info → hammad_python-0.0.32.dist-info}/licenses/LICENSE +0 -0
hammad/mcp/__init__.py
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
hammad.mcp
|
3
|
-
"""
|
4
|
-
|
5
|
-
from typing import TYPE_CHECKING
|
6
|
-
from .._internal import create_getattr_importer
|
7
|
-
|
8
|
-
if TYPE_CHECKING:
|
9
|
-
from mcp.server.fastmcp import FastMCP
|
10
|
-
from .client.client import (
|
11
|
-
convert_mcp_tool_to_openai_tool,
|
12
|
-
MCPClient,
|
13
|
-
MCPClientService,
|
14
|
-
)
|
15
|
-
from .client.settings import (
|
16
|
-
MCPClientStdioSettings,
|
17
|
-
MCPClientSseSettings,
|
18
|
-
MCPClientStreamableHttpSettings,
|
19
|
-
)
|
20
|
-
from .servers.launcher import (
|
21
|
-
launch_mcp_servers,
|
22
|
-
MCPServerService,
|
23
|
-
MCPServerStdioSettings,
|
24
|
-
MCPServerSseSettings,
|
25
|
-
MCPServerStreamableHttpSettings,
|
26
|
-
)
|
27
|
-
|
28
|
-
|
29
|
-
__all__ = (
|
30
|
-
# fastmcp
|
31
|
-
"FastMCP",
|
32
|
-
# hammad.mcp.client
|
33
|
-
"MCPClient",
|
34
|
-
"MCPClientService",
|
35
|
-
"convert_mcp_tool_to_openai_tool",
|
36
|
-
# hammad.mcp.client.settings
|
37
|
-
"MCPClientStdioSettings",
|
38
|
-
"MCPClientSseSettings",
|
39
|
-
"MCPClientStreamableHttpSettings",
|
40
|
-
# hammad.mcp.servers.launcher
|
41
|
-
"launch_mcp_servers",
|
42
|
-
"MCPServerService",
|
43
|
-
"MCPServerStdioSettings",
|
44
|
-
"MCPServerSseSettings",
|
45
|
-
"MCPServerStreamableHttpSettings",
|
46
|
-
)
|
47
|
-
|
48
|
-
|
49
|
-
__getattr__ = create_getattr_importer(__all__)
|
50
|
-
|
51
|
-
|
52
|
-
def __dir__() -> list[str]:
|
53
|
-
return list(__all__)
|
hammad/mcp/client/__init__.py
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
"""hammad.mcp.client"""
|
2
|
-
|
3
|
-
from typing import TYPE_CHECKING
|
4
|
-
from ..._internal import create_getattr_importer
|
5
|
-
|
6
|
-
if TYPE_CHECKING:
|
7
|
-
from .client import (
|
8
|
-
MCPClient,
|
9
|
-
MCPClientService,
|
10
|
-
)
|
11
|
-
from .settings import (
|
12
|
-
MCPClientSettings,
|
13
|
-
MCPClientSseSettings,
|
14
|
-
MCPClientStreamableHttpSettings,
|
15
|
-
MCPClientStdioSettings,
|
16
|
-
)
|
17
|
-
|
18
|
-
__all__ = (
|
19
|
-
# hammad.mcp.client
|
20
|
-
"MCPClient",
|
21
|
-
# hammad.mcp.client.client_service
|
22
|
-
"MCPClientService",
|
23
|
-
# hammad.mcp.client.settings
|
24
|
-
"MCPClientSettings",
|
25
|
-
"MCPClientSseSettings",
|
26
|
-
"MCPClientStreamableHttpSettings",
|
27
|
-
"MCPClientStdioSettings",
|
28
|
-
)
|
29
|
-
|
30
|
-
__getattr__ = create_getattr_importer(__all__)
|
31
|
-
|
32
|
-
|
33
|
-
def __dir__() -> list[str]:
|
34
|
-
"""Get the attributes of the client module."""
|
35
|
-
return list(__all__)
|
hammad/mcp/client/client.py
DELETED
@@ -1,624 +0,0 @@
|
|
1
|
-
"""hammad.mcp.client.client
|
2
|
-
|
3
|
-
Contains the `MCPClient` class.
|
4
|
-
"""
|
5
|
-
|
6
|
-
from __future__ import annotations
|
7
|
-
|
8
|
-
import asyncio
|
9
|
-
from dataclasses import dataclass, field
|
10
|
-
from pathlib import Path
|
11
|
-
from typing import Any, Callable, Literal, overload
|
12
|
-
import threading
|
13
|
-
import concurrent.futures
|
14
|
-
import inspect
|
15
|
-
|
16
|
-
try:
|
17
|
-
from mcp.types import CallToolResult, Tool as MCPTool
|
18
|
-
from openai.types.chat.chat_completion_tool_param import (
|
19
|
-
ChatCompletionToolParam as OpenAITool,
|
20
|
-
)
|
21
|
-
from openai.types.shared import FunctionDefinition as Function
|
22
|
-
except ImportError:
|
23
|
-
CallToolResult = Any
|
24
|
-
MCPTool = Any
|
25
|
-
OpenAITool = Any
|
26
|
-
Function = Any
|
27
|
-
|
28
|
-
from .client_service import (
|
29
|
-
MCPClientService,
|
30
|
-
MCPClientServiceSse,
|
31
|
-
MCPClientServiceStdio,
|
32
|
-
MCPClientServiceStreamableHttp,
|
33
|
-
)
|
34
|
-
from .settings import (
|
35
|
-
MCPClientSettings,
|
36
|
-
)
|
37
|
-
|
38
|
-
__all__ = (
|
39
|
-
"MCPClient",
|
40
|
-
"MCPToolWrapper",
|
41
|
-
"convert_mcp_tool_to_openai_tool",
|
42
|
-
)
|
43
|
-
|
44
|
-
|
45
|
-
# -----------------------------------------------------------------------------
|
46
|
-
# Client
|
47
|
-
# -----------------------------------------------------------------------------
|
48
|
-
|
49
|
-
|
50
|
-
def convert_mcp_tool_to_openai_tool(mcp_tool: MCPTool) -> OpenAITool:
|
51
|
-
return OpenAITool(
|
52
|
-
type="function",
|
53
|
-
function=Function(
|
54
|
-
name=mcp_tool.name,
|
55
|
-
description=mcp_tool.description,
|
56
|
-
parameters=mcp_tool.inputSchema if mcp_tool.inputSchema else {},
|
57
|
-
),
|
58
|
-
)
|
59
|
-
|
60
|
-
|
61
|
-
@dataclass
|
62
|
-
class MCPToolWrapper:
|
63
|
-
"""
|
64
|
-
Wrapper class that provides a runnable method and tool definitions
|
65
|
-
for an MCP tool.
|
66
|
-
"""
|
67
|
-
|
68
|
-
server_name: str
|
69
|
-
tool_name: str
|
70
|
-
tool_description: str
|
71
|
-
tool_args: dict[str, Any]
|
72
|
-
mcp_tool: MCPTool
|
73
|
-
openai_tool: OpenAITool
|
74
|
-
function: Callable[..., Any]
|
75
|
-
|
76
|
-
|
77
|
-
@dataclass
|
78
|
-
class MCPClient:
|
79
|
-
"""
|
80
|
-
High-level interface for connecting to MCP servers using different transports.
|
81
|
-
|
82
|
-
This class provides both synchronous and asynchronous methods for interacting
|
83
|
-
with MCP servers, wrapping the lower-level client service implementations.
|
84
|
-
"""
|
85
|
-
|
86
|
-
client_service: MCPClientService
|
87
|
-
_connected: bool = False
|
88
|
-
_sync_loop: asyncio.AbstractEventLoop = field(default=None, init=False)
|
89
|
-
_sync_thread: threading.Thread = field(default=None, init=False)
|
90
|
-
_executor: concurrent.futures.ThreadPoolExecutor = field(default=None, init=False)
|
91
|
-
|
92
|
-
@classmethod
|
93
|
-
def from_settings(
|
94
|
-
cls,
|
95
|
-
settings: MCPClientSettings,
|
96
|
-
cache_tools_list: bool = False,
|
97
|
-
name: str | None = None,
|
98
|
-
client_session_timeout_seconds: float | None = 5,
|
99
|
-
) -> MCPClient:
|
100
|
-
"""Create an MCPClient from a settings object.
|
101
|
-
|
102
|
-
Args:
|
103
|
-
settings: The MCP client settings object.
|
104
|
-
cache_tools_list: Whether to cache the tools list.
|
105
|
-
name: A readable name for the client.
|
106
|
-
client_session_timeout_seconds: The read timeout for the MCP ClientSession.
|
107
|
-
|
108
|
-
Returns:
|
109
|
-
An MCPClient instance.
|
110
|
-
"""
|
111
|
-
if settings.type == "stdio":
|
112
|
-
client_service = MCPClientServiceStdio(
|
113
|
-
settings=settings.settings,
|
114
|
-
cache_tools_list=cache_tools_list,
|
115
|
-
name=name,
|
116
|
-
client_session_timeout_seconds=client_session_timeout_seconds,
|
117
|
-
)
|
118
|
-
elif settings.type == "sse":
|
119
|
-
client_service = MCPClientServiceSse(
|
120
|
-
settings=settings.settings,
|
121
|
-
cache_tools_list=cache_tools_list,
|
122
|
-
name=name,
|
123
|
-
client_session_timeout_seconds=client_session_timeout_seconds,
|
124
|
-
)
|
125
|
-
elif settings.type == "streamable_http":
|
126
|
-
client_service = MCPClientServiceStreamableHttp(
|
127
|
-
settings=settings.settings,
|
128
|
-
cache_tools_list=cache_tools_list,
|
129
|
-
name=name,
|
130
|
-
client_session_timeout_seconds=client_session_timeout_seconds,
|
131
|
-
)
|
132
|
-
else:
|
133
|
-
raise ValueError(f"Unsupported client type: {settings.type}")
|
134
|
-
|
135
|
-
return cls(client_service=client_service)
|
136
|
-
|
137
|
-
@classmethod
|
138
|
-
def stdio(
|
139
|
-
cls,
|
140
|
-
command: str,
|
141
|
-
args: list[str] | None = None,
|
142
|
-
env: dict[str, str] | None = None,
|
143
|
-
cwd: str | Path | None = None,
|
144
|
-
encoding: str | None = None,
|
145
|
-
encoding_error_handler: Literal["strict", "ignore", "replace"] | None = None,
|
146
|
-
cache_tools_list: bool = False,
|
147
|
-
name: str | None = None,
|
148
|
-
client_session_timeout_seconds: float | None = 5,
|
149
|
-
) -> MCPClient:
|
150
|
-
"""Create an MCPClient using the stdio transport.
|
151
|
-
|
152
|
-
Args:
|
153
|
-
command: The executable to run to start the server.
|
154
|
-
args: Command line args to pass to the executable.
|
155
|
-
env: The environment variables to set for the server.
|
156
|
-
cwd: The working directory to use when spawning the process.
|
157
|
-
encoding: The text encoding used when sending/receiving messages.
|
158
|
-
encoding_error_handler: The text encoding error handler.
|
159
|
-
cache_tools_list: Whether to cache the tools list.
|
160
|
-
name: A readable name for the client.
|
161
|
-
client_session_timeout_seconds: The read timeout for the MCP ClientSession.
|
162
|
-
|
163
|
-
Returns:
|
164
|
-
An MCPClient instance.
|
165
|
-
"""
|
166
|
-
settings = MCPClientSettings.stdio(
|
167
|
-
command=command,
|
168
|
-
args=args,
|
169
|
-
env=env,
|
170
|
-
cwd=cwd,
|
171
|
-
encoding=encoding,
|
172
|
-
encoding_error_handler=encoding_error_handler,
|
173
|
-
)
|
174
|
-
|
175
|
-
return cls.from_settings(
|
176
|
-
settings=settings,
|
177
|
-
cache_tools_list=cache_tools_list,
|
178
|
-
name=name,
|
179
|
-
client_session_timeout_seconds=client_session_timeout_seconds,
|
180
|
-
)
|
181
|
-
|
182
|
-
@classmethod
|
183
|
-
def sse(
|
184
|
-
cls,
|
185
|
-
url: str,
|
186
|
-
headers: dict[str, str] | None = None,
|
187
|
-
timeout: float | None = None,
|
188
|
-
sse_read_timeout: float | None = None,
|
189
|
-
cache_tools_list: bool = False,
|
190
|
-
name: str | None = None,
|
191
|
-
client_session_timeout_seconds: float | None = 5,
|
192
|
-
) -> MCPClient:
|
193
|
-
"""Create an MCPClient using the SSE transport.
|
194
|
-
|
195
|
-
Args:
|
196
|
-
url: The URL to connect to the server.
|
197
|
-
headers: The HTTP headers to send with the request.
|
198
|
-
timeout: The timeout for the request in seconds.
|
199
|
-
sse_read_timeout: The timeout for the SSE event reads in seconds.
|
200
|
-
cache_tools_list: Whether to cache the tools list.
|
201
|
-
name: A readable name for the client.
|
202
|
-
client_session_timeout_seconds: The read timeout for the MCP ClientSession.
|
203
|
-
|
204
|
-
Returns:
|
205
|
-
An MCPClient instance.
|
206
|
-
"""
|
207
|
-
settings = MCPClientSettings.sse(
|
208
|
-
url=url,
|
209
|
-
headers=headers,
|
210
|
-
timeout=timeout,
|
211
|
-
sse_read_timeout=sse_read_timeout,
|
212
|
-
)
|
213
|
-
|
214
|
-
return cls.from_settings(
|
215
|
-
settings=settings,
|
216
|
-
cache_tools_list=cache_tools_list,
|
217
|
-
name=name,
|
218
|
-
client_session_timeout_seconds=client_session_timeout_seconds,
|
219
|
-
)
|
220
|
-
|
221
|
-
@classmethod
|
222
|
-
def streamable_http(
|
223
|
-
cls,
|
224
|
-
url: str,
|
225
|
-
headers: dict[str, str] | None = None,
|
226
|
-
timeout: float | None = None,
|
227
|
-
sse_read_timeout: float | None = None,
|
228
|
-
terminate_on_close: bool | None = None,
|
229
|
-
cache_tools_list: bool = False,
|
230
|
-
name: str | None = None,
|
231
|
-
client_session_timeout_seconds: float | None = 5,
|
232
|
-
) -> MCPClient:
|
233
|
-
"""Create an MCPClient using the streamable HTTP transport.
|
234
|
-
|
235
|
-
Args:
|
236
|
-
url: The URL to connect to the server.
|
237
|
-
headers: The HTTP headers to send with the request.
|
238
|
-
timeout: The timeout for the request in seconds.
|
239
|
-
sse_read_timeout: The timeout for the SSE event reads in seconds.
|
240
|
-
terminate_on_close: Whether to terminate the connection on close.
|
241
|
-
cache_tools_list: Whether to cache the tools list.
|
242
|
-
name: A readable name for the client.
|
243
|
-
client_session_timeout_seconds: The read timeout for the MCP ClientSession.
|
244
|
-
|
245
|
-
Returns:
|
246
|
-
An MCPClient instance.
|
247
|
-
"""
|
248
|
-
settings = MCPClientSettings.streamable_http(
|
249
|
-
url=url,
|
250
|
-
headers=headers,
|
251
|
-
timeout=timeout,
|
252
|
-
sse_read_timeout=sse_read_timeout,
|
253
|
-
terminate_on_close=terminate_on_close,
|
254
|
-
)
|
255
|
-
|
256
|
-
return cls.from_settings(
|
257
|
-
settings=settings,
|
258
|
-
cache_tools_list=cache_tools_list,
|
259
|
-
name=name,
|
260
|
-
client_session_timeout_seconds=client_session_timeout_seconds,
|
261
|
-
)
|
262
|
-
|
263
|
-
@property
|
264
|
-
def name(self) -> str:
|
265
|
-
"""A readable name for the client."""
|
266
|
-
return self.client_service.name
|
267
|
-
|
268
|
-
def _ensure_sync_context(self):
|
269
|
-
"""Ensure we have a persistent async context for sync operations."""
|
270
|
-
if self._sync_loop is None or self._sync_loop.is_closed():
|
271
|
-
self._create_sync_context()
|
272
|
-
|
273
|
-
def _create_sync_context(self):
|
274
|
-
"""Create a persistent async context for sync operations."""
|
275
|
-
if self._executor:
|
276
|
-
self._executor.shutdown(wait=False)
|
277
|
-
|
278
|
-
self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
279
|
-
|
280
|
-
def run_loop():
|
281
|
-
loop = asyncio.new_event_loop()
|
282
|
-
asyncio.set_event_loop(loop)
|
283
|
-
self._sync_loop = loop
|
284
|
-
try:
|
285
|
-
loop.run_forever()
|
286
|
-
finally:
|
287
|
-
# Clean up when the loop stops
|
288
|
-
try:
|
289
|
-
if self._connected:
|
290
|
-
loop.run_until_complete(self.async_cleanup())
|
291
|
-
except Exception:
|
292
|
-
pass # Ignore cleanup errors
|
293
|
-
loop.close()
|
294
|
-
|
295
|
-
self._sync_thread = threading.Thread(target=run_loop, daemon=True)
|
296
|
-
self._sync_thread.start()
|
297
|
-
|
298
|
-
# Wait for the loop to be ready
|
299
|
-
import time
|
300
|
-
|
301
|
-
timeout = 5.0
|
302
|
-
start_time = time.time()
|
303
|
-
while (self._sync_loop is None or not self._sync_loop.is_running()) and (
|
304
|
-
time.time() - start_time
|
305
|
-
) < timeout:
|
306
|
-
time.sleep(0.01)
|
307
|
-
|
308
|
-
if self._sync_loop is None or not self._sync_loop.is_running():
|
309
|
-
raise RuntimeError("Failed to start sync event loop")
|
310
|
-
|
311
|
-
def _run_in_sync_context(self, coro):
|
312
|
-
"""Run a coroutine in the persistent sync context."""
|
313
|
-
self._ensure_sync_context()
|
314
|
-
|
315
|
-
future = asyncio.run_coroutine_threadsafe(coro, self._sync_loop)
|
316
|
-
return future.result()
|
317
|
-
|
318
|
-
def connect(self) -> None:
|
319
|
-
"""Connect to the MCP server synchronously."""
|
320
|
-
self._run_in_sync_context(self.async_connect())
|
321
|
-
|
322
|
-
async def async_connect(self) -> None:
|
323
|
-
"""Connect to the MCP server asynchronously."""
|
324
|
-
if self._connected:
|
325
|
-
return
|
326
|
-
await self.client_service.connect()
|
327
|
-
self._connected = True
|
328
|
-
|
329
|
-
def cleanup(self) -> None:
|
330
|
-
"""Cleanup the client connection synchronously."""
|
331
|
-
try:
|
332
|
-
if self._connected:
|
333
|
-
self._run_in_sync_context(self.async_cleanup())
|
334
|
-
finally:
|
335
|
-
# Clean up the sync context
|
336
|
-
if self._sync_loop and not self._sync_loop.is_closed():
|
337
|
-
self._sync_loop.call_soon_threadsafe(self._sync_loop.stop)
|
338
|
-
if self._executor:
|
339
|
-
self._executor.shutdown(wait=True)
|
340
|
-
self._executor = None
|
341
|
-
self._sync_loop = None
|
342
|
-
self._sync_thread = None
|
343
|
-
|
344
|
-
async def async_cleanup(self) -> None:
|
345
|
-
"""Cleanup the client connection asynchronously."""
|
346
|
-
if not self._connected:
|
347
|
-
return
|
348
|
-
await self.client_service.cleanup()
|
349
|
-
self._connected = False
|
350
|
-
|
351
|
-
def list_tools(self) -> list[MCPTool]:
|
352
|
-
"""List the tools available on the server synchronously.
|
353
|
-
|
354
|
-
Returns:
|
355
|
-
A list of available MCP tools.
|
356
|
-
"""
|
357
|
-
return self._run_in_sync_context(self.async_list_tools())
|
358
|
-
|
359
|
-
def list_wrapped_tools(self) -> list[MCPToolWrapper]:
|
360
|
-
"""List the tools available on the server as wrapped tools with OpenAI compatibility.
|
361
|
-
|
362
|
-
Returns:
|
363
|
-
A list of MCPToolWrapper objects that include both MCP and OpenAI tool formats,
|
364
|
-
plus callable functions for each tool.
|
365
|
-
"""
|
366
|
-
# Get the raw MCP tools
|
367
|
-
mcp_tools = self.list_tools()
|
368
|
-
|
369
|
-
wrapped_tools = []
|
370
|
-
for mcp_tool in mcp_tools:
|
371
|
-
# Convert to OpenAI tool format
|
372
|
-
openai_tool = convert_mcp_tool_to_openai_tool(mcp_tool)
|
373
|
-
|
374
|
-
# Create a callable function for this tool
|
375
|
-
def create_tool_function(tool_name: str):
|
376
|
-
def tool_function(**kwargs) -> Any:
|
377
|
-
"""Dynamically created function that calls the MCP tool."""
|
378
|
-
return self.call_tool(tool_name, kwargs if kwargs else None)
|
379
|
-
|
380
|
-
# Set function metadata
|
381
|
-
tool_function.__name__ = tool_name
|
382
|
-
tool_function.__doc__ = f"MCP tool: {mcp_tool.description}"
|
383
|
-
|
384
|
-
return tool_function
|
385
|
-
|
386
|
-
# Extract tool arguments from input schema
|
387
|
-
tool_args = {}
|
388
|
-
if mcp_tool.inputSchema and isinstance(mcp_tool.inputSchema, dict):
|
389
|
-
properties = mcp_tool.inputSchema.get("properties", {})
|
390
|
-
for prop_name, prop_info in properties.items():
|
391
|
-
if isinstance(prop_info, dict):
|
392
|
-
tool_args[prop_name] = prop_info.get("type", "any")
|
393
|
-
else:
|
394
|
-
tool_args[prop_name] = "any"
|
395
|
-
|
396
|
-
# Create the wrapper
|
397
|
-
wrapper = MCPToolWrapper(
|
398
|
-
server_name=self.name,
|
399
|
-
tool_name=mcp_tool.name,
|
400
|
-
tool_description=mcp_tool.description or "",
|
401
|
-
tool_args=tool_args,
|
402
|
-
mcp_tool=mcp_tool,
|
403
|
-
openai_tool=openai_tool,
|
404
|
-
function=create_tool_function(mcp_tool.name),
|
405
|
-
)
|
406
|
-
|
407
|
-
wrapped_tools.append(wrapper)
|
408
|
-
|
409
|
-
return wrapped_tools
|
410
|
-
|
411
|
-
async def async_list_tools(self) -> list[MCPTool]:
|
412
|
-
"""List the tools available on the server asynchronously.
|
413
|
-
|
414
|
-
Returns:
|
415
|
-
A list of available MCP tools.
|
416
|
-
"""
|
417
|
-
if not self._connected:
|
418
|
-
await self.async_connect()
|
419
|
-
return await self.client_service.list_tools()
|
420
|
-
|
421
|
-
def call_tool(
|
422
|
-
self, tool_name: str, arguments: dict[str, Any] | None = None
|
423
|
-
) -> CallToolResult:
|
424
|
-
"""Invoke a tool on the server synchronously.
|
425
|
-
|
426
|
-
Args:
|
427
|
-
tool_name: The name of the tool to call.
|
428
|
-
arguments: The arguments to pass to the tool.
|
429
|
-
|
430
|
-
Returns:
|
431
|
-
The result of the tool call.
|
432
|
-
"""
|
433
|
-
return self._run_in_sync_context(self.async_call_tool(tool_name, arguments))
|
434
|
-
|
435
|
-
async def async_call_tool(
|
436
|
-
self, tool_name: str, arguments: dict[str, Any] | None = None
|
437
|
-
) -> CallToolResult:
|
438
|
-
"""Invoke a tool on the server asynchronously.
|
439
|
-
|
440
|
-
Args:
|
441
|
-
tool_name: The name of the tool to call.
|
442
|
-
arguments: The arguments to pass to the tool.
|
443
|
-
|
444
|
-
Returns:
|
445
|
-
The result of the tool call.
|
446
|
-
"""
|
447
|
-
if not self._connected:
|
448
|
-
await self.async_connect()
|
449
|
-
return await self.client_service.call_tool(tool_name, arguments)
|
450
|
-
|
451
|
-
def as_tool(
|
452
|
-
self, tool_name: str, func: Callable[..., Any] | None = None
|
453
|
-
) -> Callable[..., Any]:
|
454
|
-
"""Decorator to convert a function into an MCP tool call.
|
455
|
-
|
456
|
-
This decorator allows you to use a function as if it were a local function,
|
457
|
-
but it will actually call the corresponding MCP tool.
|
458
|
-
|
459
|
-
Args:
|
460
|
-
tool_name: The name of the MCP tool to call.
|
461
|
-
func: The function to decorate (optional, for decorator factory pattern).
|
462
|
-
|
463
|
-
Returns:
|
464
|
-
A decorated function that calls the MCP tool.
|
465
|
-
|
466
|
-
Usage:
|
467
|
-
@client.as_tool("my_tool")
|
468
|
-
def my_function(arg1, arg2):
|
469
|
-
pass
|
470
|
-
|
471
|
-
# Or as a factory:
|
472
|
-
my_function = client.as_tool("my_tool")
|
473
|
-
"""
|
474
|
-
|
475
|
-
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
476
|
-
def wrapper(*args, **kwargs) -> Any:
|
477
|
-
# Get the function signature to map arguments properly
|
478
|
-
sig = inspect.signature(f)
|
479
|
-
parameters = list(sig.parameters.keys())
|
480
|
-
|
481
|
-
# Create a dictionary mapping positional args to parameter names
|
482
|
-
arguments = {}
|
483
|
-
|
484
|
-
# Map positional arguments to parameter names
|
485
|
-
for i, arg in enumerate(args):
|
486
|
-
if i < len(parameters):
|
487
|
-
arguments[parameters[i]] = arg
|
488
|
-
else:
|
489
|
-
# If there are more positional args than parameters, use generic names
|
490
|
-
arguments[f"arg_{i}"] = arg
|
491
|
-
|
492
|
-
# Add keyword arguments (these override positional if there's a conflict)
|
493
|
-
arguments.update(kwargs)
|
494
|
-
|
495
|
-
# Call the MCP tool
|
496
|
-
result = self.call_tool(tool_name, arguments if arguments else None)
|
497
|
-
return result
|
498
|
-
|
499
|
-
return wrapper
|
500
|
-
|
501
|
-
if func is None:
|
502
|
-
# Used as @client.as_tool("tool_name")
|
503
|
-
return decorator
|
504
|
-
else:
|
505
|
-
# Used as @client.as_tool("tool_name", func)
|
506
|
-
return decorator(func)
|
507
|
-
|
508
|
-
def __enter__(self) -> MCPClient:
|
509
|
-
"""Context manager entry."""
|
510
|
-
self.connect()
|
511
|
-
return self
|
512
|
-
|
513
|
-
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
514
|
-
"""Context manager exit."""
|
515
|
-
self.cleanup()
|
516
|
-
|
517
|
-
async def __aenter__(self) -> MCPClient:
|
518
|
-
"""Async context manager entry."""
|
519
|
-
await self.async_connect()
|
520
|
-
return self
|
521
|
-
|
522
|
-
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
523
|
-
"""Async context manager exit."""
|
524
|
-
await self.async_cleanup()
|
525
|
-
|
526
|
-
|
527
|
-
# -----------------------------------------------------------------------------
|
528
|
-
# Factory Function
|
529
|
-
# -----------------------------------------------------------------------------
|
530
|
-
|
531
|
-
|
532
|
-
@overload
|
533
|
-
def create_mcp_client(
|
534
|
-
type: Literal["stdio"],
|
535
|
-
*,
|
536
|
-
command: str,
|
537
|
-
args: list[str] | None = None,
|
538
|
-
env: dict[str, str] | None = None,
|
539
|
-
cwd: Path | str | None = None,
|
540
|
-
timeout: float = 30.0,
|
541
|
-
) -> MCPClient:
|
542
|
-
"""Create an MCP client with stdio transport."""
|
543
|
-
...
|
544
|
-
|
545
|
-
|
546
|
-
@overload
|
547
|
-
def create_mcp_client(
|
548
|
-
type: Literal["sse"],
|
549
|
-
*,
|
550
|
-
url: str,
|
551
|
-
timeout: float = 30.0,
|
552
|
-
) -> MCPClient:
|
553
|
-
"""Create an MCP client with SSE transport."""
|
554
|
-
...
|
555
|
-
|
556
|
-
|
557
|
-
@overload
|
558
|
-
def create_mcp_client(
|
559
|
-
type: Literal["http"],
|
560
|
-
*,
|
561
|
-
url: str,
|
562
|
-
timeout: float = 30.0,
|
563
|
-
) -> MCPClient:
|
564
|
-
"""Create an MCP client with HTTP transport."""
|
565
|
-
...
|
566
|
-
|
567
|
-
|
568
|
-
def create_mcp_client(
|
569
|
-
type: Literal["stdio", "sse", "http"],
|
570
|
-
*,
|
571
|
-
command: str | None = None,
|
572
|
-
args: list[str] | None = None,
|
573
|
-
env: dict[str, str] | None = None,
|
574
|
-
cwd: Path | str | None = None,
|
575
|
-
url: str | None = None,
|
576
|
-
timeout: float = 30.0,
|
577
|
-
) -> MCPClient:
|
578
|
-
"""Create an MCP client with the specified transport type.
|
579
|
-
|
580
|
-
Args:
|
581
|
-
service_type: The type of transport to use ("stdio", "sse", or "http").
|
582
|
-
command: Command to run for stdio transport.
|
583
|
-
args: Arguments for the command (stdio only).
|
584
|
-
env: Environment variables for the command (stdio only).
|
585
|
-
cwd: Working directory for the command (stdio only).
|
586
|
-
url: URL for SSE or HTTP transport.
|
587
|
-
timeout: Connection timeout in seconds.
|
588
|
-
|
589
|
-
Returns:
|
590
|
-
A configured MCPClient instance.
|
591
|
-
|
592
|
-
Raises:
|
593
|
-
ValueError: If required parameters for the transport type are missing.
|
594
|
-
"""
|
595
|
-
service_type = type
|
596
|
-
|
597
|
-
if service_type == "stdio":
|
598
|
-
if command is None:
|
599
|
-
raise ValueError("command is required for stdio transport")
|
600
|
-
|
601
|
-
service = MCPClientServiceStdio(
|
602
|
-
command=command,
|
603
|
-
args=args or [],
|
604
|
-
env=env or {},
|
605
|
-
cwd=Path(cwd) if cwd else None,
|
606
|
-
)
|
607
|
-
|
608
|
-
elif service_type == "sse":
|
609
|
-
if url is None:
|
610
|
-
raise ValueError("url is required for SSE transport")
|
611
|
-
|
612
|
-
service = MCPClientServiceSse(url=url)
|
613
|
-
|
614
|
-
elif service_type == "http":
|
615
|
-
if url is None:
|
616
|
-
raise ValueError("url is required for HTTP transport")
|
617
|
-
|
618
|
-
service = MCPClientServiceStreamableHttp(url=url)
|
619
|
-
|
620
|
-
else:
|
621
|
-
raise ValueError(f"Unsupported service_type: {service_type}")
|
622
|
-
|
623
|
-
settings = MCPClientSettings(timeout=timeout)
|
624
|
-
return MCPClient(service=service, settings=settings)
|