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.
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/PKG-INFO +2 -1
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/pyproject.toml +2 -1
- universal_mcp-0.1.22/src/tests/test_applications.py +71 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_tool.py +7 -8
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/applications/__init__.py +5 -1
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/integrations/integration.py +4 -8
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/servers/server.py +16 -31
- universal_mcp-0.1.22/src/universal_mcp/tools/adapters.py +104 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/tools/func_metadata.py +4 -2
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/tools/manager.py +122 -37
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/tools/tools.py +1 -1
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/agentr.py +27 -13
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/docstring_parser.py +18 -64
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/api_splitter.py +250 -132
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/openapi.py +224 -99
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/preprocessor.py +272 -29
- universal_mcp-0.1.22/src/universal_mcp/utils/testing.py +31 -0
- universal_mcp-0.1.20rc2/src/tests/test_applications.py +0 -92
- universal_mcp-0.1.20rc2/src/universal_mcp/tools/adapters.py +0 -68
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/.gitignore +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/LICENSE +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/README.md +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/__init__.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/conftest.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_api_generator.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_api_integration.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_localserver.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_stores.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_tool_manager.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/tests/test_zenquotes.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/__init__.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/analytics.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/applications/README.md +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/applications/application.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/cli.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/config.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/exceptions.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/integrations/README.md +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/integrations/__init__.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/logger.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/py.typed +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/servers/README.md +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/servers/__init__.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/stores/README.md +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/stores/__init__.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/stores/store.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/tools/README.md +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/tools/__init__.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/__init__.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/common.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/installation.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/__inti__.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/api_generator.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/docgen.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/openapi/readme.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/singleton.py +0 -0
- {universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/utils/templates/README.md.j2 +0 -0
- {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.
|
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.
|
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": "
|
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", "
|
44
|
-
"b": {"type": "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": "
|
92
|
-
"b": {"type": "string", "title": "
|
93
|
-
"c": {"type": "number", "title": "
|
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": "
|
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
|
{universal_mcp-0.1.20rc2 → universal_mcp-0.1.22}/src/universal_mcp/integrations/integration.py
RENAMED
@@ -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
|
-
|
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
|
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
|
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=
|
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
|
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.
|
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
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
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
|
196
|
-
field_info.title =
|
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,
|