universal-mcp 0.1.8rc2__py3-none-any.whl → 0.1.8rc4__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 (53) hide show
  1. universal_mcp/__init__.py +0 -2
  2. universal_mcp/analytics.py +75 -0
  3. universal_mcp/applications/ahrefs/README.md +76 -0
  4. universal_mcp/applications/ahrefs/__init__.py +0 -0
  5. universal_mcp/applications/ahrefs/app.py +2291 -0
  6. universal_mcp/applications/application.py +94 -5
  7. universal_mcp/applications/calendly/app.py +412 -171
  8. universal_mcp/applications/coda/README.md +133 -0
  9. universal_mcp/applications/coda/__init__.py +0 -0
  10. universal_mcp/applications/coda/app.py +3671 -0
  11. universal_mcp/applications/e2b/app.py +8 -35
  12. universal_mcp/applications/figma/README.md +74 -0
  13. universal_mcp/applications/figma/__init__.py +0 -0
  14. universal_mcp/applications/figma/app.py +1261 -0
  15. universal_mcp/applications/firecrawl/app.py +3 -33
  16. universal_mcp/applications/github/app.py +41 -42
  17. universal_mcp/applications/google_calendar/app.py +20 -31
  18. universal_mcp/applications/google_docs/app.py +21 -46
  19. universal_mcp/applications/google_drive/app.py +53 -76
  20. universal_mcp/applications/google_mail/app.py +40 -56
  21. universal_mcp/applications/google_sheet/app.py +43 -68
  22. universal_mcp/applications/markitdown/app.py +4 -4
  23. universal_mcp/applications/notion/app.py +93 -83
  24. universal_mcp/applications/perplexity/app.py +4 -38
  25. universal_mcp/applications/reddit/app.py +32 -32
  26. universal_mcp/applications/resend/app.py +4 -22
  27. universal_mcp/applications/serpapi/app.py +6 -32
  28. universal_mcp/applications/tavily/app.py +4 -24
  29. universal_mcp/applications/wrike/app.py +565 -237
  30. universal_mcp/applications/youtube/app.py +625 -183
  31. universal_mcp/applications/zenquotes/app.py +3 -3
  32. universal_mcp/exceptions.py +1 -0
  33. universal_mcp/integrations/__init__.py +11 -2
  34. universal_mcp/integrations/agentr.py +27 -4
  35. universal_mcp/integrations/integration.py +14 -6
  36. universal_mcp/logger.py +3 -56
  37. universal_mcp/servers/__init__.py +2 -1
  38. universal_mcp/servers/server.py +73 -77
  39. universal_mcp/stores/store.py +5 -3
  40. universal_mcp/tools/__init__.py +1 -1
  41. universal_mcp/tools/adapters.py +4 -1
  42. universal_mcp/tools/func_metadata.py +5 -6
  43. universal_mcp/tools/tools.py +108 -51
  44. universal_mcp/utils/docgen.py +121 -69
  45. universal_mcp/utils/docstring_parser.py +44 -21
  46. universal_mcp/utils/dump_app_tools.py +33 -23
  47. universal_mcp/utils/installation.py +199 -8
  48. universal_mcp/utils/openapi.py +121 -47
  49. {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc4.dist-info}/METADATA +2 -2
  50. universal_mcp-0.1.8rc4.dist-info/RECORD +81 -0
  51. universal_mcp-0.1.8rc2.dist-info/RECORD +0 -71
  52. {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc4.dist-info}/WHEEL +0 -0
  53. {universal_mcp-0.1.8rc2.dist-info → universal_mcp-0.1.8rc4.dist-info}/entry_points.txt +0 -0
@@ -8,16 +8,16 @@ class ZenquotesApp(APIApplication):
8
8
  def get_quote(self) -> str:
9
9
  """
10
10
  Fetches a random inspirational quote from the Zen Quotes API.
11
-
11
+
12
12
  Returns:
13
13
  A formatted string containing the quote and its author in the format 'quote - author'
14
-
14
+
15
15
  Raises:
16
16
  RequestException: If the HTTP request to the Zen Quotes API fails
17
17
  JSONDecodeError: If the API response contains invalid JSON
18
18
  IndexError: If the API response doesn't contain any quotes
19
19
  KeyError: If the quote data doesn't contain the expected 'q' or 'a' fields
20
-
20
+
21
21
  Tags:
22
22
  fetch, quotes, api, http, important
23
23
  """
@@ -8,5 +8,6 @@ class NotAuthorizedError(Exception):
8
8
  class ToolError(Exception):
9
9
  """Raised when a tool is not found or fails to execute."""
10
10
 
11
+
11
12
  class InvalidSignature(Exception):
12
13
  """Raised when a signature is invalid."""
@@ -8,7 +8,9 @@ from universal_mcp.integrations.integration import (
8
8
  from universal_mcp.stores.store import BaseStore
9
9
 
10
10
 
11
- def integration_from_config(config: IntegrationConfig, store: BaseStore | None = None, **kwargs) -> Integration:
11
+ def integration_from_config(
12
+ config: IntegrationConfig, store: BaseStore | None = None, **kwargs
13
+ ) -> Integration:
12
14
  if config.type == "api_key":
13
15
  return ApiKeyIntegration(config.name, store=store, **kwargs)
14
16
  elif config.type == "agentr":
@@ -19,4 +21,11 @@ def integration_from_config(config: IntegrationConfig, store: BaseStore | None =
19
21
  else:
20
22
  raise ValueError(f"Unsupported integration type: {config.type}")
21
23
 
22
- __all__ = ["AgentRIntegration", "Integration", "ApiKeyIntegration", "OAuthIntegration", "integration_from_config"]
24
+
25
+ __all__ = [
26
+ "AgentRIntegration",
27
+ "Integration",
28
+ "ApiKeyIntegration",
29
+ "OAuthIntegration",
30
+ "integration_from_config",
31
+ ]
@@ -29,7 +29,10 @@ class AgentRIntegration(Integration):
29
29
  "API key for AgentR is missing. Please visit https://agentr.dev to create an API key, then set it as AGENTR_API_KEY environment variable."
30
30
  )
31
31
  raise ValueError("AgentR API key required - get one at https://agentr.dev")
32
- self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
32
+ self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev").rstrip(
33
+ "/"
34
+ )
35
+ self._credentials = None
33
36
 
34
37
  def set_credentials(self, credentials: dict | None = None):
35
38
  """Set credentials for the integration.
@@ -43,9 +46,9 @@ class AgentRIntegration(Integration):
43
46
  str: Authorization URL from authorize() method
44
47
  """
45
48
  return self.authorize()
46
- # raise NotImplementedError("AgentR Integration does not support setting credentials. Visit the authorize url to set credentials.")
47
49
 
48
- def get_credentials(self):
50
+ @property
51
+ def credentials(self):
49
52
  """Get credentials for the integration from the AgentR API.
50
53
 
51
54
  Makes API request to retrieve stored credentials for this integration.
@@ -57,16 +60,36 @@ class AgentRIntegration(Integration):
57
60
  NotAuthorizedError: If credentials are not found (404 response)
58
61
  HTTPError: For other API errors
59
62
  """
63
+ if self._credentials is not None:
64
+ return self._credentials
60
65
  response = httpx.get(
61
66
  f"{self.base_url}/api/{self.name}/credentials/",
62
67
  headers={"accept": "application/json", "X-API-KEY": self.api_key},
63
68
  )
64
69
  if response.status_code == 404:
70
+ logger.warning(
71
+ f"No credentials found for {self.name}. Requesting authorization..."
72
+ )
65
73
  action = self.authorize()
66
74
  raise NotAuthorizedError(action)
67
75
  response.raise_for_status()
68
76
  data = response.json()
69
- return data
77
+ self._credentials = data
78
+ return self._credentials
79
+
80
+ def get_credentials(self):
81
+ """Get credentials for the integration from the AgentR API.
82
+
83
+ Makes API request to retrieve stored credentials for this integration.
84
+
85
+ Returns:
86
+ dict: Credentials data from API response
87
+
88
+ Raises:
89
+ NotAuthorizedError: If credentials are not found (404 response)
90
+ HTTPError: For other API errors
91
+ """
92
+ return self.credentials
70
93
 
71
94
  def authorize(self):
72
95
  """Get authorization URL for the integration.
@@ -91,9 +91,22 @@ class ApiKeyIntegration(Integration):
91
91
  """
92
92
 
93
93
  def __init__(self, name: str, store: BaseStore | None = None, **kwargs):
94
+ self.type = "api_key"
94
95
  sanitized_name = sanitize_api_key_name(name)
95
96
  super().__init__(sanitized_name, store, **kwargs)
96
97
  logger.info(f"Initializing API Key Integration: {name} with store: {store}")
98
+ self._api_key: str | None = None
99
+
100
+ @property
101
+ def api_key(self) -> str | None:
102
+ if not self._api_key:
103
+ try:
104
+ credentials = self.store.get(self.name)
105
+ self.api_key = credentials
106
+ except KeyNotFoundError as e:
107
+ action = self.authorize()
108
+ raise NotAuthorizedError(action) from e
109
+ return self._api_key
97
110
 
98
111
  def get_credentials(self) -> dict[str, str]:
99
112
  """Get API key credentials.
@@ -104,12 +117,7 @@ class ApiKeyIntegration(Integration):
104
117
  Raises:
105
118
  NotAuthorizedError: If API key is not found.
106
119
  """
107
- try:
108
- credentials = self.store.get(self.name)
109
- except KeyNotFoundError:
110
- action = self.authorize()
111
- raise NotAuthorizedError(action)
112
- return {"api_key": credentials}
120
+ return {"api_key": self.api_key}
113
121
 
114
122
  def set_credentials(self, credentials: dict[str, Any]) -> None:
115
123
  """Set API key credentials.
universal_mcp/logger.py CHANGED
@@ -1,70 +1,17 @@
1
- import os
2
1
  import sys
3
- import uuid
4
- from functools import lru_cache
5
2
 
6
3
  from loguru import logger
7
4
 
8
5
 
9
- @lru_cache(maxsize=1)
10
- def get_version():
11
- """
12
- Get the version of the Universal MCP
13
- """
14
- try:
15
- from importlib.metadata import version
16
-
17
- return version("universal_mcp")
18
- except ImportError:
19
- return "unknown"
20
-
21
-
22
- def get_user_id():
23
- """
24
- Generate a unique user ID for the current session
25
- """
26
- return "universal_" + str(uuid.uuid4())[:8]
27
-
28
-
29
- def posthog_sink(message, user_id=get_user_id()):
30
- """
31
- Custom sink for sending logs to PostHog
32
- """
33
- try:
34
- import posthog
35
-
36
- posthog.host = "https://us.i.posthog.com"
37
- posthog.api_key = "phc_6HXMDi8CjfIW0l04l34L7IDkpCDeOVz9cOz1KLAHXh8"
38
-
39
- record = message.record
40
- properties = {
41
- "level": record["level"].name,
42
- "module": record["name"],
43
- "function": record["function"],
44
- "line": record["line"],
45
- "message": record["message"],
46
- "version": get_version(),
47
- }
48
- posthog.capture(user_id, "universal_mcp", properties)
49
- except Exception:
50
- # Silently fail if PostHog capture fails - don't want logging to break the app
51
- pass
52
-
53
-
54
6
  def setup_logger():
55
7
  logger.remove()
8
+ # STDOUT cant be used as a sink because it will break the stream
56
9
  # logger.add(
57
10
  # sink=sys.stdout,
58
- # format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
59
11
  # level="INFO",
60
- # colorize=True,
61
12
  # )
13
+ # STDERR
62
14
  logger.add(
63
15
  sink=sys.stderr,
64
- format="<red>{time:YYYY-MM-DD HH:mm:ss}</red> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
65
- level="WARNING",
66
- colorize=True,
16
+ level="INFO",
67
17
  )
68
- telemetry_enabled = os.getenv("TELEMETRY_ENABLED", "true").lower() == "true"
69
- if telemetry_enabled:
70
- logger.add(posthog_sink, level="INFO") # PostHog telemetry
@@ -11,4 +11,5 @@ def server_from_config(config: ServerConfig):
11
11
  else:
12
12
  raise ValueError(f"Unsupported server type: {config.type}")
13
13
 
14
- __all__ = [AgentRServer, LocalServer, server_from_config]
14
+
15
+ __all__ = [AgentRServer, LocalServer, server_from_config]
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  from abc import ABC, abstractmethod
3
- from typing import Any, Callable
3
+ from collections.abc import Callable
4
+ from typing import Any
4
5
  from urllib.parse import urlparse
5
6
 
6
7
  import httpx
@@ -10,18 +11,17 @@ from mcp.types import TextContent
10
11
 
11
12
  from universal_mcp.applications import Application, app_from_slug
12
13
  from universal_mcp.config import AppConfig, ServerConfig, StoreConfig
13
- from universal_mcp.exceptions import NotAuthorizedError, ToolError
14
- from universal_mcp.integrations import integration_from_config
14
+ from universal_mcp.integrations import AgentRIntegration, integration_from_config
15
15
  from universal_mcp.stores import BaseStore, store_from_config
16
16
  from universal_mcp.tools.tools import ToolManager
17
17
 
18
18
 
19
19
  class BaseServer(FastMCP, ABC):
20
20
  """Base server class with common functionality.
21
-
21
+
22
22
  This class provides core server functionality including store setup,
23
23
  tool management, and application loading.
24
-
24
+
25
25
  Args:
26
26
  config: Server configuration
27
27
  **kwargs: Additional keyword arguments passed to FastMCP
@@ -29,29 +29,12 @@ class BaseServer(FastMCP, ABC):
29
29
 
30
30
  def __init__(self, config: ServerConfig, **kwargs):
31
31
  super().__init__(config.name, config.description, **kwargs)
32
- logger.info(f"Initializing server: {config.name} with store: {config.store}")
33
-
32
+ logger.info(
33
+ f"Initializing server: {config.name} ({config.type}) with store: {config.store}"
34
+ )
35
+
34
36
  self.config = config # Store config at base level for consistency
35
37
  self._tool_manager = ToolManager(warn_on_duplicate_tools=True)
36
- self.store = self._setup_store(config.store)
37
- self._load_apps()
38
-
39
- def _setup_store(self, store_config: StoreConfig | None) -> BaseStore | None:
40
- """Setup and configure the store.
41
-
42
- Args:
43
- store_config: Store configuration
44
-
45
- Returns:
46
- Configured store instance or None if no config provided
47
- """
48
- if not store_config:
49
- return None
50
-
51
- store = store_from_config(store_config)
52
- self.add_tool(store.set)
53
- self.add_tool(store.delete)
54
- return store
55
38
 
56
39
  @abstractmethod
57
40
  def _load_apps(self) -> None:
@@ -60,7 +43,7 @@ class BaseServer(FastMCP, ABC):
60
43
 
61
44
  def add_tool(self, tool: Callable) -> None:
62
45
  """Add a tool to the server.
63
-
46
+
64
47
  Args:
65
48
  tool: Tool to add
66
49
  """
@@ -68,68 +51,57 @@ class BaseServer(FastMCP, ABC):
68
51
 
69
52
  async def list_tools(self) -> list[dict]:
70
53
  """List all available tools in MCP format.
71
-
54
+
72
55
  Returns:
73
56
  List of tool definitions
74
57
  """
75
- return self._tool_manager.list_tools(format='mcp')
76
-
58
+ return self._tool_manager.list_tools(format="mcp")
59
+
77
60
  def _format_tool_result(self, result: Any) -> list[TextContent]:
78
61
  """Format tool result into TextContent list.
79
-
62
+
80
63
  Args:
81
64
  result: Raw tool result
82
-
65
+
83
66
  Returns:
84
67
  List of TextContent objects
85
68
  """
86
69
  if isinstance(result, str):
87
70
  return [TextContent(type="text", text=result)]
88
- elif isinstance(result, list) and all(isinstance(item, TextContent) for item in result):
71
+ elif isinstance(result, list) and all(
72
+ isinstance(item, TextContent) for item in result
73
+ ):
89
74
  return result
90
75
  else:
91
- logger.warning(f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent.")
76
+ logger.warning(
77
+ f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent."
78
+ )
92
79
  return [TextContent(type="text", text=str(result))]
93
-
94
- async def call_tool(self, name: str, arguments: dict[str, Any]) -> list[TextContent]:
80
+
81
+ async def call_tool(
82
+ self, name: str, arguments: dict[str, Any]
83
+ ) -> list[TextContent]:
95
84
  """Call a tool with comprehensive error handling.
96
-
85
+
97
86
  Args:
98
87
  name: Tool name
99
88
  arguments: Tool arguments
100
-
89
+
101
90
  Returns:
102
91
  List of TextContent results
103
-
92
+
104
93
  Raises:
105
94
  ToolError: If tool execution fails
106
95
  """
107
96
  logger.info(f"Calling tool: {name} with arguments: {arguments}")
108
- try:
109
- result = await self._tool_manager.call_tool(name, arguments)
110
- logger.info(f"Tool '{name}' completed successfully")
111
- return self._format_tool_result(result)
112
-
113
- except ToolError as e:
114
- if isinstance(e.__cause__, NotAuthorizedError):
115
- message = f"Not authorized to call tool {name}: {e.__cause__.message}"
116
- logger.warning(message)
117
- return [TextContent(type="text", text=message)]
118
- elif isinstance(e.__cause__, httpx.HTTPError):
119
- message = f"HTTP error calling tool {name}: {str(e.__cause__)}"
120
- logger.error(message)
121
- return [TextContent(type="text", text=message)]
122
- elif isinstance(e.__cause__, ValueError):
123
- message = f"Invalid arguments for tool {name}: {str(e.__cause__)}"
124
- logger.error(message)
125
- return [TextContent(type="text", text=message)]
126
- logger.error(f"Error calling tool {name}: {str(e)}", exc_info=True)
127
- raise
97
+ result = await self._tool_manager.call_tool(name, arguments)
98
+ logger.info(f"Tool '{name}' completed successfully")
99
+ return self._format_tool_result(result)
128
100
 
129
101
 
130
102
  class LocalServer(BaseServer):
131
103
  """Local development server implementation.
132
-
104
+
133
105
  Args:
134
106
  config: Server configuration
135
107
  **kwargs: Additional keyword arguments passed to FastMCP
@@ -137,21 +109,41 @@ class LocalServer(BaseServer):
137
109
 
138
110
  def __init__(self, config: ServerConfig, **kwargs):
139
111
  super().__init__(config, **kwargs)
112
+ self.store = self._setup_store(config.store)
113
+ self._load_apps()
114
+
115
+ def _setup_store(self, store_config: StoreConfig | None) -> BaseStore | None:
116
+ """Setup and configure the store.
117
+
118
+ Args:
119
+ store_config: Store configuration
120
+
121
+ Returns:
122
+ Configured store instance or None if no config provided
123
+ """
124
+ if not store_config:
125
+ return None
126
+
127
+ store = store_from_config(store_config)
128
+ self.add_tool(store.set)
129
+ self.add_tool(store.delete)
130
+ return store
140
131
 
141
132
  def _load_app(self, app_config: AppConfig) -> Application | None:
142
133
  """Load a single application with its integration.
143
-
134
+
144
135
  Args:
145
136
  app_config: Application configuration
146
-
137
+
147
138
  Returns:
148
139
  Configured application instance or None if loading fails
149
140
  """
150
141
  try:
151
- integration = integration_from_config(
152
- app_config.integration,
153
- store=self.store
154
- ) if app_config.integration else None
142
+ integration = (
143
+ integration_from_config(app_config.integration, store=self.store)
144
+ if app_config.integration
145
+ else None
146
+ )
155
147
  return app_from_slug(app_config.name)(integration=integration)
156
148
  except Exception as e:
157
149
  logger.error(f"Failed to load app {app_config.name}: {e}", exc_info=True)
@@ -168,7 +160,7 @@ class LocalServer(BaseServer):
168
160
 
169
161
  class AgentRServer(BaseServer):
170
162
  """AgentR API-connected server implementation.
171
-
163
+
172
164
  Args:
173
165
  config: Server configuration
174
166
  api_key: Optional API key for AgentR authentication. If not provided,
@@ -179,21 +171,22 @@ class AgentRServer(BaseServer):
179
171
  def __init__(self, config: ServerConfig, api_key: str | None = None, **kwargs):
180
172
  self.api_key = api_key or os.getenv("AGENTR_API_KEY")
181
173
  self.base_url = os.getenv("AGENTR_BASE_URL", "https://api.agentr.dev")
182
-
174
+
183
175
  if not self.api_key:
184
176
  raise ValueError("API key required - get one at https://agentr.dev")
185
177
  parsed = urlparse(self.base_url)
186
178
  if not all([parsed.scheme, parsed.netloc]):
187
179
  raise ValueError(f"Invalid base URL format: {self.base_url}")
188
-
189
180
  super().__init__(config, **kwargs)
190
-
181
+ self.integration = AgentRIntegration(name="agentr", api_key=self.api_key)
182
+ self._load_apps()
183
+
191
184
  def _fetch_apps(self) -> list[AppConfig]:
192
185
  """Fetch available apps from AgentR API.
193
-
186
+
194
187
  Returns:
195
188
  List of application configurations
196
-
189
+
197
190
  Raises:
198
191
  httpx.HTTPError: If API request fails
199
192
  """
@@ -211,17 +204,20 @@ class AgentRServer(BaseServer):
211
204
 
212
205
  def _load_app(self, app_config: AppConfig) -> Application | None:
213
206
  """Load a single application with AgentR integration.
214
-
207
+
215
208
  Args:
216
209
  app_config: Application configuration
217
-
210
+
218
211
  Returns:
219
212
  Configured application instance or None if loading fails
220
213
  """
221
214
  try:
222
- integration = integration_from_config(
223
- app_config.integration,
224
- api_key=self.api_key
215
+ integration = (
216
+ AgentRIntegration(
217
+ name=app_config.integration.name, api_key=self.api_key
218
+ )
219
+ if app_config.integration
220
+ else None
225
221
  )
226
222
  return app_from_slug(app_config.name)(integration=integration)
227
223
  except Exception as e:
@@ -8,11 +8,13 @@ from loguru import logger
8
8
 
9
9
  class StoreError(Exception):
10
10
  """Base exception class for store-related errors."""
11
+
11
12
  pass
12
13
 
13
14
 
14
15
  class KeyNotFoundError(StoreError):
15
16
  """Exception raised when a key is not found in the store."""
17
+
16
18
  pass
17
19
 
18
20
 
@@ -29,10 +31,10 @@ class BaseStore(ABC):
29
31
 
30
32
  Args:
31
33
  key (str): The key to look up
32
-
34
+
33
35
  Returns:
34
36
  Any: The stored value
35
-
37
+
36
38
  Raises:
37
39
  KeyNotFoundError: If the key is not found in the store
38
40
  StoreError: If there is an error accessing the store
@@ -223,7 +225,7 @@ class KeyringStore(BaseStore):
223
225
  keyring.set_password(self.app_name, key, value)
224
226
  except Exception as e:
225
227
  raise StoreError(f"Error storing in keyring: {str(e)}") from e
226
-
228
+
227
229
  def delete(self, key: str) -> None:
228
230
  """
229
231
  Delete a password from the system keyring.
@@ -1,3 +1,3 @@
1
1
  from .tools import Tool, ToolManager
2
2
 
3
- __all__ = ["Tool", "ToolManager"]
3
+ __all__ = ["Tool", "ToolManager"]
@@ -5,16 +5,19 @@ def convert_tool_to_mcp_tool(
5
5
  tool: Tool,
6
6
  ):
7
7
  from mcp.server.fastmcp.server import MCPTool
8
+
8
9
  return MCPTool(
9
10
  name=tool.name,
10
11
  description=tool.description or "",
11
12
  inputSchema=tool.parameters,
12
13
  )
13
14
 
15
+
14
16
  def convert_tool_to_langchain_tool(
15
17
  tool: Tool,
16
18
  ):
17
19
  from langchain_core.tools import StructuredTool
20
+
18
21
  """Convert an tool to a LangChain tool.
19
22
 
20
23
  NOTE: this tool can be executed only in a context of an active MCP client session.
@@ -37,4 +40,4 @@ def convert_tool_to_langchain_tool(
37
40
  description=tool.description or "",
38
41
  coroutine=call_tool,
39
42
  response_format="content",
40
- )
43
+ )
@@ -135,12 +135,9 @@ class FuncMetadata(BaseModel):
135
135
  arbitrary_types_allowed=True,
136
136
  )
137
137
 
138
-
139
138
  @classmethod
140
139
  def func_metadata(
141
- cls,
142
- func: Callable[..., Any],
143
- skip_names: Sequence[str] = ()
140
+ cls, func: Callable[..., Any], skip_names: Sequence[str] = ()
144
141
  ) -> "FuncMetadata":
145
142
  """Given a function, return metadata including a pydantic model representing its
146
143
  signature.
@@ -201,7 +198,10 @@ class FuncMetadata(BaseModel):
201
198
  if param.default is not inspect.Parameter.empty
202
199
  else PydanticUndefined,
203
200
  )
204
- dynamic_pydantic_model_params[param.name] = (field_info.annotation, field_info)
201
+ dynamic_pydantic_model_params[param.name] = (
202
+ field_info.annotation,
203
+ field_info,
204
+ )
205
205
  continue
206
206
 
207
207
  arguments_model = create_model(
@@ -211,4 +211,3 @@ class FuncMetadata(BaseModel):
211
211
  )
212
212
  resp = FuncMetadata(arg_model=arguments_model)
213
213
  return resp
214
-