universal-mcp 0.1.20rc2__tar.gz → 0.1.22__tar.gz

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 (58) hide show
  1. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/PKG-INFO +2 -1
  2. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/pyproject.toml +2 -1
  3. universal_mcp-0.1.22/src/tests/test_applications.py +71 -0
  4. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_tool.py +7 -8
  5. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/applications/__init__.py +5 -1
  6. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/integrations/integration.py +4 -8
  7. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/servers/server.py +16 -31
  8. universal_mcp-0.1.22/src/universal_mcp/tools/adapters.py +104 -0
  9. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/tools/func_metadata.py +4 -2
  10. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/tools/manager.py +122 -37
  11. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/tools/tools.py +1 -1
  12. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/agentr.py +27 -13
  13. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/docstring_parser.py +18 -64
  14. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/api_splitter.py +250 -132
  15. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/openapi.py +224 -99
  16. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/preprocessor.py +272 -29
  17. universal_mcp-0.1.22/src/universal_mcp/utils/testing.py +31 -0
  18. universal_mcp-0.1.20rc2/src/tests/test_applications.py +0 -92
  19. universal_mcp-0.1.20rc2/src/universal_mcp/tools/adapters.py +0 -68
  20. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/.gitignore +0 -0
  21. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/LICENSE +0 -0
  22. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/README.md +0 -0
  23. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/__init__.py +0 -0
  24. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/conftest.py +0 -0
  25. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_api_generator.py +0 -0
  26. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_api_integration.py +0 -0
  27. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_localserver.py +0 -0
  28. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_stores.py +0 -0
  29. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_tool_manager.py +0 -0
  30. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_zenquotes.py +0 -0
  31. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/__init__.py +0 -0
  32. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/analytics.py +0 -0
  33. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/applications/README.md +0 -0
  34. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/applications/application.py +0 -0
  35. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/cli.py +0 -0
  36. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/config.py +0 -0
  37. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/exceptions.py +0 -0
  38. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/integrations/README.md +0 -0
  39. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/integrations/__init__.py +0 -0
  40. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/logger.py +0 -0
  41. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/py.typed +0 -0
  42. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/servers/README.md +0 -0
  43. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/servers/__init__.py +0 -0
  44. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/stores/README.md +0 -0
  45. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/stores/__init__.py +0 -0
  46. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/stores/store.py +0 -0
  47. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/tools/README.md +0 -0
  48. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/tools/__init__.py +0 -0
  49. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/__init__.py +0 -0
  50. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/common.py +0 -0
  51. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/installation.py +0 -0
  52. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/__inti__.py +0 -0
  53. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/api_generator.py +0 -0
  54. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/docgen.py +0 -0
  55. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/readme.py +0 -0
  56. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/singleton.py +0 -0
  57. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/templates/README.md.j2 +0 -0
  58. {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/templates/api_client.py.j2 +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: universal-mcp
3
- Version: 0.1.20rc2
3
+ Version: 0.1.22
4
4
  Summary: Universal MCP acts as a middle ware for your API applications. It can store your credentials, authorize, enable disable apps on the fly and much more.
5
5
  Author-email: Manoj Bajaj <manojbajaj95@gmail.com>
6
6
  License: MIT
7
7
  License-File: LICENSE
8
8
  Requires-Python: >=3.11
9
+ Requires-Dist: black>=25.1.0
9
10
  Requires-Dist: cookiecutter>=2.6.0
10
11
  Requires-Dist: gql[all]>=3.5.2
11
12
  Requires-Dist: jinja2>=3.1.3
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "universal-mcp"
7
- version = "0.1.20-rc2"
7
+ version = "0.1.22"
8
8
  description = "Universal MCP acts as a middle ware for your API applications. It can store your credentials, authorize, enable disable apps on the fly and much more."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -14,6 +14,7 @@ requires-python = ">=3.11"
14
14
  license = { text = "MIT" }
15
15
  dependencies = [
16
16
  "Jinja2>=3.1.3",
17
+ "black>=25.1.0",
17
18
  "cookiecutter>=2.6.0",
18
19
  "gql[all]>=3.5.2",
19
20
  "jsonref>=1.1.0",
@@ -0,0 +1,71 @@
1
+ import pytest
2
+
3
+ from universal_mcp.applications import app_from_slug
4
+ from universal_mcp.utils.testing import check_application_instance
5
+
6
+ ALL_APPS = [
7
+ "ahrefs",
8
+ "airtable",
9
+ "apollo",
10
+ "asana",
11
+ "box",
12
+ # "braze",
13
+ # "cal-com-v2",
14
+ "confluence",
15
+ "calendly",
16
+ "canva",
17
+ # "clickup",
18
+ "coda",
19
+ "crustdata",
20
+ "e2b",
21
+ # "elevenlabs",
22
+ "exa",
23
+ "falai",
24
+ "figma",
25
+ "firecrawl",
26
+ "github",
27
+ "gong",
28
+ "google-calendar",
29
+ "google-docs",
30
+ # "google-drive",
31
+ "google-gemini",
32
+ "google-mail",
33
+ "google-sheet",
34
+ "hashnode",
35
+ "heygen",
36
+ "hubspot",
37
+ # "jira",
38
+ "klaviyo",
39
+ "mailchimp",
40
+ "markitdown",
41
+ "miro",
42
+ "neon",
43
+ "notion",
44
+ "perplexity",
45
+ # "pipedrive",
46
+ "posthog",
47
+ "reddit",
48
+ "replicate",
49
+ "resend",
50
+ "retell",
51
+ "rocketlane",
52
+ "serpapi",
53
+ # "shopify",
54
+ "shortcut",
55
+ "spotify",
56
+ "supabase",
57
+ "tavily",
58
+ "trello",
59
+ "unipile",
60
+ # "whatsapp-business",
61
+ "wrike",
62
+ "youtube",
63
+ "zenquotes",
64
+ ]
65
+
66
+
67
+ @pytest.mark.parametrize("app_name", ALL_APPS)
68
+ def test_application(app_name):
69
+ app = app_from_slug(app_name)
70
+ app_instance = app(integration=None)
71
+ check_application_instance(app_instance, app_name)
@@ -18,7 +18,7 @@ def test_func_metadata_annotated():
18
18
  "title": "funcArguments",
19
19
  "properties": {
20
20
  "a": {"type": "integer", "title": "First integer"},
21
- "b": {"type": "integer", "title": "B"},
21
+ "b": {"type": "integer", "title": "b"},
22
22
  },
23
23
  "required": ["a", "b"],
24
24
  }
@@ -40,8 +40,8 @@ def test_func_metadata_no_annotated():
40
40
  "type": "object",
41
41
  "title": "funcArguments",
42
42
  "properties": {
43
- "a": {"type": "integer", "title": "The first integer"},
44
- "b": {"type": "integer", "title": "The second integer"},
43
+ "a": {"type": "integer", "description": "The first integer", "title": "a"},
44
+ "b": {"type": "integer", "description": "The second integer", "title": "b"},
45
45
  },
46
46
  "required": ["a", "b"],
47
47
  }
@@ -57,7 +57,6 @@ def test_func_metadata_no_args():
57
57
  "type": "object",
58
58
  "title": "funcArguments",
59
59
  "properties": {},
60
- # "required": [],
61
60
  }
62
61
 
63
62
 
@@ -88,9 +87,9 @@ def test_func_metadata_required():
88
87
  "type": "object",
89
88
  "title": "funcArguments",
90
89
  "properties": {
91
- "a": {"type": "integer", "title": "A"},
92
- "b": {"type": "string", "title": "B"},
93
- "c": {"type": "number", "title": "C", "default": 1.0},
90
+ "a": {"type": "integer", "title": "a"},
91
+ "b": {"type": "string", "title": "b"},
92
+ "c": {"type": "number", "title": "c", "default": 1.0},
94
93
  },
95
94
  "required": ["a", "b"],
96
95
  }
@@ -106,7 +105,7 @@ def test_func_metadata_none_type():
106
105
  "type": "object",
107
106
  "title": "funcArguments",
108
107
  "properties": {
109
- "a": {"type": "null", "title": "A", "default": None},
108
+ "a": {"type": "null", "title": "a", "default": None},
110
109
  },
111
110
  }
112
111
 
@@ -30,12 +30,13 @@ sys.path.append(str(UNIVERSAL_MCP_HOME))
30
30
  # Name are in the format of "app-name", eg, google-calendar
31
31
  # Class name is NameApp, eg, GoogleCalendarApp
32
32
 
33
+ app_cache: dict[str, type[BaseApplication]] = {}
34
+
33
35
 
34
36
  def _install_or_upgrade_package(package_name: str, repository_path: str):
35
37
  """
36
38
  Helper to install a package via pip from the universal-mcp GitHub repository.
37
39
  """
38
-
39
40
  uv_path = os.getenv("UV_PATH")
40
41
  uv_executable = str(Path(uv_path) / "uv") if uv_path else "uv"
41
42
  logger.info(f"Using uv executable: {uv_executable}")
@@ -72,6 +73,8 @@ def app_from_slug(slug: str):
72
73
  Dynamically resolve and return the application class for the given slug.
73
74
  Attempts installation from GitHub if the package is not found locally.
74
75
  """
76
+ if slug in app_cache:
77
+ return app_cache[slug]
75
78
  class_name = get_default_class_name(slug)
76
79
  module_path = get_default_module_path(slug)
77
80
  package_name = get_default_package_name(slug)
@@ -82,6 +85,7 @@ def app_from_slug(slug: str):
82
85
  module = importlib.import_module(module_path)
83
86
  class_ = getattr(module, class_name)
84
87
  logger.debug(f"Loaded class '{class_}' from module '{module_path}'")
88
+ app_cache[slug] = class_
85
89
  return class_
86
90
  except ModuleNotFoundError as e:
87
91
  raise ModuleNotFoundError(f"Package '{module_path}' not found locally. Please install it first.") from e
@@ -3,9 +3,8 @@ from typing import Any
3
3
  import httpx
4
4
  from loguru import logger
5
5
 
6
- from universal_mcp.exceptions import NotAuthorizedError
7
- from universal_mcp.stores import BaseStore
8
- from universal_mcp.stores.store import KeyNotFoundError, MemoryStore
6
+ from universal_mcp.exceptions import KeyNotFoundError, NotAuthorizedError
7
+ from universal_mcp.stores import BaseStore, MemoryStore
9
8
  from universal_mcp.utils.agentr import AgentrClient
10
9
 
11
10
 
@@ -34,10 +33,7 @@ class Integration:
34
33
 
35
34
  def __init__(self, name: str, store: BaseStore | None = None):
36
35
  self.name = name
37
- if store is None:
38
- self.store = MemoryStore()
39
- else:
40
- self.store = store
36
+ self.store = store or MemoryStore()
41
37
 
42
38
  def authorize(self) -> str | dict[str, Any]:
43
39
  """Authorize the integration.
@@ -329,7 +325,7 @@ class AgentRIntegration(Integration):
329
325
  ValueError: If no API key is provided or found in environment variables
330
326
  """
331
327
 
332
- def __init__(self, name: str, api_key: str = None, **kwargs):
328
+ def __init__(self, name: str, api_key: str, **kwargs):
333
329
  super().__init__(name, **kwargs)
334
330
  self.client = AgentrClient(api_key=api_key)
335
331
  self._credentials = None
@@ -13,6 +13,7 @@ from universal_mcp.exceptions import ConfigurationError, ToolError
13
13
  from universal_mcp.integrations import AgentRIntegration, integration_from_config
14
14
  from universal_mcp.stores import BaseStore, store_from_config
15
15
  from universal_mcp.tools import ToolManager
16
+ from universal_mcp.tools.adapters import ToolFormat, format_to_mcp_result
16
17
  from universal_mcp.utils.agentr import AgentrClient
17
18
 
18
19
 
@@ -33,6 +34,7 @@ class BaseServer(FastMCP):
33
34
  logger.info(f"Initializing server: {config.name} ({config.type}) with store: {config.store}")
34
35
  self.config = config
35
36
  self._tool_manager = tool_manager or ToolManager(warn_on_duplicate_tools=True)
37
+ # Validate config after setting attributes to ensure proper initialization
36
38
  ServerConfig.model_validate(config)
37
39
  except Exception as e:
38
40
  logger.error(f"Failed to initialize server: {e}", exc_info=True)
@@ -47,33 +49,15 @@ class BaseServer(FastMCP):
47
49
  Raises:
48
50
  ValueError: If tool is invalid
49
51
  """
50
-
51
52
  self._tool_manager.add_tool(tool)
52
53
 
53
- async def list_tools(self) -> list[dict]:
54
+ async def list_tools(self) -> list:
54
55
  """List all available tools in MCP format.
55
56
 
56
57
  Returns:
57
58
  List of tool definitions
58
59
  """
59
- return self._tool_manager.list_tools(format="mcp")
60
-
61
- def _format_tool_result(self, result: Any) -> list[TextContent]:
62
- """Format tool result into TextContent list.
63
-
64
- Args:
65
- result: Raw tool result
66
-
67
- Returns:
68
- List of TextContent objects
69
- """
70
- if isinstance(result, str):
71
- return [TextContent(type="text", text=result)]
72
- elif isinstance(result, list) and all(isinstance(item, TextContent) for item in result):
73
- return result
74
- else:
75
- logger.warning(f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent.")
76
- return [TextContent(type="text", text=str(result))]
60
+ return self._tool_manager.list_tools(format=ToolFormat.MCP)
77
61
 
78
62
  async def call_tool(self, name: str, arguments: dict[str, Any]) -> list[TextContent]:
79
63
  """Call a tool with comprehensive error handling.
@@ -98,7 +82,7 @@ class BaseServer(FastMCP):
98
82
  try:
99
83
  result = await self._tool_manager.call_tool(name, arguments)
100
84
  logger.info(f"Tool '{name}' completed successfully")
101
- return self._format_tool_result(result)
85
+ return format_to_mcp_result(result)
102
86
  except Exception as e:
103
87
  logger.error(f"Tool '{name}' failed: {e}", exc_info=True)
104
88
  raise ToolError(f"Tool execution failed: {str(e)}") from e
@@ -218,11 +202,12 @@ class AgentRServer(BaseServer):
218
202
  """
219
203
 
220
204
  def __init__(self, config: ServerConfig, **kwargs):
205
+ super().__init__(config, **kwargs)
221
206
  self.api_key = config.api_key.get_secret_value() if config.api_key else None
207
+ if not self.api_key:
208
+ raise ValueError("API key is required for AgentR server")
222
209
  logger.info(f"Initializing AgentR server with API key: {self.api_key}")
223
210
  self.client = AgentrClient(api_key=self.api_key)
224
- super().__init__(config, **kwargs)
225
- self.integration = AgentRIntegration(name="agentr", api_key=self.client.api_key)
226
211
  self._load_apps()
227
212
 
228
213
  def _fetch_apps(self) -> list[AppConfig]:
@@ -260,7 +245,7 @@ class AgentRServer(BaseServer):
260
245
  """
261
246
  try:
262
247
  integration = (
263
- AgentRIntegration(name=app_config.integration.name, api_key=self.client.api_key)
248
+ AgentRIntegration(name=app_config.integration.name, api_key=self.api_key)
264
249
  if app_config.integration
265
250
  else None
266
251
  )
@@ -321,12 +306,12 @@ class SingleMCPServer(BaseServer):
321
306
  **kwargs,
322
307
  ):
323
308
  if not app_instance:
324
- raise ValueError("app_instance is required")
325
- if not config:
326
- config = ServerConfig(
327
- type="local",
328
- name=f"{app_instance.name.title()} MCP Server for Local Development",
329
- description=f"Minimal MCP server for the local {app_instance.name} application.",
330
- )
309
+ raise ValueError("app_instance is required for SingleMCPServer")
310
+
311
+ config = config or ServerConfig(
312
+ type="local",
313
+ name=f"{app_instance.name.title()} MCP Server for Local Development",
314
+ description=f"Minimal MCP server for the local {app_instance.name} application.",
315
+ )
331
316
  super().__init__(config, **kwargs)
332
317
  self._tool_manager.register_tools_from_app(app_instance, tags="all")
@@ -0,0 +1,104 @@
1
+ from enum import Enum
2
+
3
+ from loguru import logger
4
+ from mcp.types import TextContent
5
+
6
+ from universal_mcp.tools.tools import Tool
7
+
8
+
9
+ class ToolFormat(str, Enum):
10
+ """Supported tool formats."""
11
+
12
+ MCP = "mcp"
13
+ LANGCHAIN = "langchain"
14
+ OPENAI = "openai"
15
+
16
+
17
+ def convert_tool_to_mcp_tool(
18
+ tool: Tool,
19
+ ):
20
+ from mcp.server.fastmcp.server import MCPTool
21
+
22
+ logger.debug(f"Converting tool '{tool.name}' to MCP format")
23
+ mcp_tool = MCPTool(
24
+ name=tool.name[:63],
25
+ description=tool.description or "",
26
+ inputSchema=tool.parameters,
27
+ )
28
+ logger.debug(f"Successfully converted tool '{tool.name}' to MCP format")
29
+ return mcp_tool
30
+
31
+
32
+ def format_to_mcp_result(result: any) -> list[TextContent]:
33
+ """Format tool result into TextContent list.
34
+
35
+ Args:
36
+ result: Raw tool result
37
+
38
+ Returns:
39
+ List of TextContent objects
40
+ """
41
+ logger.debug(f"Formatting result to MCP format, type: {type(result)}")
42
+ if isinstance(result, str):
43
+ logger.debug("Result is string, wrapping in TextContent")
44
+ return [TextContent(type="text", text=result)]
45
+ elif isinstance(result, list) and all(isinstance(item, TextContent) for item in result):
46
+ logger.debug("Result is already list of TextContent objects")
47
+ return result
48
+ else:
49
+ logger.warning(f"Tool returned unexpected type: {type(result)}. Wrapping in TextContent.")
50
+ return [TextContent(type="text", text=str(result))]
51
+
52
+
53
+ def convert_tool_to_langchain_tool(
54
+ tool: Tool,
55
+ ):
56
+ from langchain_core.tools import StructuredTool
57
+
58
+ """Convert an tool to a LangChain tool.
59
+
60
+ NOTE: this tool can be executed only in a context of an active MCP client session.
61
+
62
+ Args:
63
+ tool: Tool to convert
64
+
65
+ Returns:
66
+ a LangChain tool
67
+ """
68
+
69
+ logger.debug(f"Converting tool '{tool.name}' to LangChain format")
70
+
71
+ async def call_tool(
72
+ **arguments: dict[str, any],
73
+ ):
74
+ logger.debug(f"Executing LangChain tool '{tool.name}' with arguments: {arguments}")
75
+ call_tool_result = await tool.run(arguments)
76
+ logger.debug(f"Tool '{tool.name}' execution completed")
77
+ return call_tool_result
78
+
79
+ langchain_tool = StructuredTool(
80
+ name=tool.name,
81
+ description=tool.description or "",
82
+ coroutine=call_tool,
83
+ response_format="content",
84
+ args_schema=tool.parameters,
85
+ )
86
+ logger.debug(f"Successfully converted tool '{tool.name}' to LangChain format")
87
+ return langchain_tool
88
+
89
+
90
+ def convert_tool_to_openai_tool(
91
+ tool: Tool,
92
+ ):
93
+ """Convert a Tool object to an OpenAI function."""
94
+ logger.debug(f"Converting tool '{tool.name}' to OpenAI format")
95
+ openai_tool = {
96
+ "type": "function",
97
+ "function": {
98
+ "name": tool.name,
99
+ "description": tool.description,
100
+ "parameters": tool.parameters,
101
+ },
102
+ }
103
+ logger.debug(f"Successfully converted tool '{tool.name}' to OpenAI format")
104
+ return openai_tool
@@ -192,8 +192,10 @@ class FuncMetadata(BaseModel):
192
192
  _get_typed_annotation(annotation, globalns),
193
193
  param.default if param.default is not inspect.Parameter.empty else PydanticUndefined,
194
194
  )
195
- if not field_info.title and arg_description and arg_description.get(param.name):
196
- field_info.title = arg_description.get(param.name)
195
+ if not field_info.title:
196
+ field_info.title = param.name
197
+ if not field_info.description and arg_description and arg_description.get(param.name):
198
+ field_info.description = arg_description.get(param.name)
197
199
  dynamic_pydantic_model_params[param.name] = (
198
200
  field_info.annotation,
199
201
  field_info,