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