universal-mcp 0.1.14__py3-none-any.whl → 0.1.15rc7__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 (34) hide show
  1. universal_mcp/analytics.py +7 -1
  2. universal_mcp/applications/README.md +122 -0
  3. universal_mcp/applications/__init__.py +48 -46
  4. universal_mcp/applications/application.py +249 -40
  5. universal_mcp/cli.py +49 -49
  6. universal_mcp/config.py +95 -22
  7. universal_mcp/exceptions.py +8 -0
  8. universal_mcp/integrations/integration.py +18 -2
  9. universal_mcp/logger.py +59 -8
  10. universal_mcp/servers/__init__.py +2 -2
  11. universal_mcp/stores/store.py +2 -12
  12. universal_mcp/tools/__init__.py +14 -2
  13. universal_mcp/tools/adapters.py +25 -0
  14. universal_mcp/tools/func_metadata.py +12 -2
  15. universal_mcp/tools/manager.py +236 -0
  16. universal_mcp/tools/tools.py +5 -249
  17. universal_mcp/utils/common.py +33 -0
  18. universal_mcp/utils/openapi/__inti__.py +0 -0
  19. universal_mcp/utils/{api_generator.py → openapi/api_generator.py} +1 -1
  20. universal_mcp/utils/openapi/openapi.py +930 -0
  21. universal_mcp/utils/openapi/preprocessor.py +1223 -0
  22. universal_mcp/utils/{readme.py → openapi/readme.py} +21 -31
  23. universal_mcp/utils/templates/README.md.j2 +17 -0
  24. {universal_mcp-0.1.14.dist-info → universal_mcp-0.1.15rc7.dist-info}/METADATA +6 -3
  25. universal_mcp-0.1.15rc7.dist-info/RECORD +44 -0
  26. universal_mcp-0.1.15rc7.dist-info/licenses/LICENSE +21 -0
  27. universal_mcp/templates/README.md.j2 +0 -93
  28. universal_mcp/utils/dump_app_tools.py +0 -78
  29. universal_mcp/utils/openapi.py +0 -697
  30. universal_mcp-0.1.14.dist-info/RECORD +0 -39
  31. /universal_mcp/utils/{docgen.py → openapi/docgen.py} +0 -0
  32. /universal_mcp/{templates → utils/templates}/api_client.py.j2 +0 -0
  33. {universal_mcp-0.1.14.dist-info → universal_mcp-0.1.15rc7.dist-info}/WHEEL +0 -0
  34. {universal_mcp-0.1.14.dist-info → universal_mcp-0.1.15rc7.dist-info}/entry_points.txt +0 -0
universal_mcp/cli.py CHANGED
@@ -2,7 +2,7 @@ import re
2
2
  from pathlib import Path
3
3
 
4
4
  import typer
5
- from rich import print as rprint
5
+ from rich.console import Console
6
6
  from rich.panel import Panel
7
7
 
8
8
  from universal_mcp.utils.installation import (
@@ -11,6 +11,9 @@ from universal_mcp.utils.installation import (
11
11
  install_cursor,
12
12
  )
13
13
 
14
+ # Setup rich console and logging
15
+ console = Console()
16
+
14
17
  app = typer.Typer()
15
18
 
16
19
 
@@ -36,10 +39,10 @@ def generate(
36
39
  This name will be used for the folder in applications/.
37
40
  """
38
41
  # Import here to avoid circular imports
39
- from universal_mcp.utils.api_generator import generate_api_from_schema
42
+ from universal_mcp.utils.openapi.api_generator import generate_api_from_schema
40
43
 
41
44
  if not schema_path.exists():
42
- typer.echo(f"Error: Schema file {schema_path} does not exist", err=True)
45
+ console.print(f"[red]Error: Schema file {schema_path} does not exist[/red]")
43
46
  raise typer.Exit(1)
44
47
 
45
48
  try:
@@ -49,28 +52,22 @@ def generate(
49
52
  output_path=output_path,
50
53
  class_name=class_name,
51
54
  )
52
- typer.echo("API client successfully generated and installed.")
53
- typer.echo(f"Application file: {app_file}")
55
+ console.print("[green]API client successfully generated and installed.[/green]")
56
+ console.print(f"[blue]Application file: {app_file}[/blue]")
54
57
  except Exception as e:
55
- typer.echo(f"Error generating API client: {e}", err=True)
58
+ console.print(f"[red]Error generating API client: {e}[/red]")
56
59
  raise typer.Exit(1) from e
57
60
 
58
61
 
59
62
  @app.command()
60
63
  def readme(
61
64
  file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
62
- class_name: str = typer.Option(
63
- None,
64
- "--class-name",
65
- "-c",
66
- help="Class name to use for the API client",
67
- ),
68
65
  ):
69
66
  """Generate a README.md file for the API client."""
70
- from universal_mcp.utils.readme import generate_readme
67
+ from universal_mcp.utils.openapi.readme import generate_readme
71
68
 
72
- readme_file = generate_readme(file_path, class_name)
73
- typer.echo(f"README.md file generated at: {readme_file}")
69
+ readme_file = generate_readme(file_path)
70
+ console.print(f"[green]README.md file generated at: {readme_file}[/green]")
74
71
 
75
72
 
76
73
  @app.command()
@@ -88,17 +85,17 @@ def docgen(
88
85
  This command uses litellm with structured output to generate high-quality
89
86
  Google-style docstrings for all functions in the specified Python file.
90
87
  """
91
- from universal_mcp.utils.docgen import process_file
88
+ from universal_mcp.utils.openapi.docgen import process_file
92
89
 
93
90
  if not file_path.exists():
94
- typer.echo(f"Error: File not found: {file_path}", err=True)
91
+ console.print(f"[red]Error: File not found: {file_path}[/red]")
95
92
  raise typer.Exit(1)
96
93
 
97
94
  try:
98
95
  processed = process_file(str(file_path), model)
99
- typer.echo(f"Successfully processed {processed} functions")
96
+ console.print(f"[green]Successfully processed {processed} functions[/green]")
100
97
  except Exception as e:
101
- typer.echo(f"Error: {e}", err=True)
98
+ console.print(f"[red]Error: {e}[/red]")
102
99
  raise typer.Exit(1) from e
103
100
 
104
101
 
@@ -130,15 +127,14 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
130
127
  supported_apps = get_supported_apps()
131
128
 
132
129
  if app_name not in supported_apps:
133
- typer.echo("Available apps:")
130
+ console.print("[yellow]Available apps:[/yellow]")
134
131
  for app in supported_apps:
135
- typer.echo(f" - {app}")
136
- typer.echo(f"\nApp '{app_name}' not supported")
132
+ console.print(f" - {app}")
133
+ console.print(f"\n[red]App '{app_name}' not supported[/red]")
137
134
  raise typer.Exit(1)
138
135
 
139
136
  # Print instructions before asking for API key
140
-
141
- rprint(
137
+ console.print(
142
138
  Panel(
143
139
  "API key is required. Visit [link]https://agentr.dev[/link] to create an API key.",
144
140
  title="Instruction",
@@ -156,15 +152,15 @@ def install(app_name: str = typer.Argument(..., help="Name of app to install")):
156
152
  )
157
153
  try:
158
154
  if app_name == "claude":
159
- typer.echo(f"Installing mcp server for: {app_name}")
155
+ console.print(f"[blue]Installing mcp server for: {app_name}[/blue]")
160
156
  install_claude(api_key)
161
- typer.echo("App installed successfully")
157
+ console.print("[green]App installed successfully[/green]")
162
158
  elif app_name == "cursor":
163
- typer.echo(f"Installing mcp server for: {app_name}")
159
+ console.print(f"[blue]Installing mcp server for: {app_name}[/blue]")
164
160
  install_cursor(api_key)
165
- typer.echo("App installed successfully")
161
+ console.print("[green]App installed successfully[/green]")
166
162
  except Exception as e:
167
- typer.echo(f"Error installing app: {e}", err=True)
163
+ console.print(f"[red]Error installing app: {e}[/red]")
168
164
  raise typer.Exit(1) from e
169
165
 
170
166
 
@@ -198,9 +194,8 @@ def init(
198
194
 
199
195
  def validate_pattern(value: str, field_name: str) -> None:
200
196
  if not re.match(NAME_PATTERN, value):
201
- typer.secho(
202
- f"❌ Invalid {field_name}; only letters, numbers, hyphens, and underscores allowed.",
203
- fg=typer.colors.RED,
197
+ console.print(
198
+ f"[red]❌ Invalid {field_name}; only letters, numbers, hyphens, and underscores allowed.[/red]"
204
199
  )
205
200
  raise typer.Exit(code=1)
206
201
 
@@ -212,7 +207,7 @@ def init(
212
207
  prompt_suffix=" (e.g., reddit, youtube): ",
213
208
  ).strip()
214
209
  validate_pattern(app_name, "app name")
215
-
210
+ app_name = app_name.lower()
216
211
  if not output_dir:
217
212
  path_str = typer.prompt(
218
213
  "Enter the output directory for the project",
@@ -224,20 +219,17 @@ def init(
224
219
  if not output_dir.exists():
225
220
  try:
226
221
  output_dir.mkdir(parents=True, exist_ok=True)
227
- typer.secho(
228
- f"✅ Created output directory at '{output_dir}'",
229
- fg=typer.colors.GREEN,
222
+ console.print(
223
+ f"[green]✅ Created output directory at '{output_dir}'[/green]"
230
224
  )
231
225
  except Exception as e:
232
- typer.secho(
233
- f"❌ Failed to create output directory '{output_dir}': {e}",
234
- fg=typer.colors.RED,
226
+ console.print(
227
+ f"[red]❌ Failed to create output directory '{output_dir}': {e}[/red]"
235
228
  )
236
229
  raise typer.Exit(code=1) from e
237
230
  elif not output_dir.is_dir():
238
- typer.secho(
239
- f"❌ Output path '{output_dir}' exists but is not a directory.",
240
- fg=typer.colors.RED,
231
+ console.print(
232
+ f"[red]❌ Output path '{output_dir}' exists but is not a directory.[/red]"
241
233
  )
242
234
  raise typer.Exit(code=1)
243
235
 
@@ -249,13 +241,12 @@ def init(
249
241
  prompt_suffix=" (api_key, oauth, agentr, none): ",
250
242
  ).lower()
251
243
  if integration_type not in ("api_key", "oauth", "agentr", "none"):
252
- typer.secho(
253
- "❌ Integration type must be one of: api_key, oauth, agentr, none",
254
- fg=typer.colors.RED,
244
+ console.print(
245
+ "[red]❌ Integration type must be one of: api_key, oauth, agentr, none[/red]"
255
246
  )
256
247
  raise typer.Exit(code=1)
257
248
 
258
- typer.secho("🚀 Generating project using cookiecutter...", fg=typer.colors.BLUE)
249
+ console.print("[blue]🚀 Generating project using cookiecutter...[/blue]")
259
250
  try:
260
251
  cookiecutter(
261
252
  "https://github.com/AgentrDev/universal-mcp-app-template.git",
@@ -267,11 +258,20 @@ def init(
267
258
  },
268
259
  )
269
260
  except Exception as exc:
270
- typer.secho(f"❌ Project generation failed: {exc}", fg=typer.colors.RED)
261
+ console.print(f"❌ Project generation failed: {exc}")
271
262
  raise typer.Exit(code=1) from exc
272
263
 
273
- project_dir = output_dir / f"universal-mcp-{app_name}"
274
- typer.secho(f"✅ Project created at {project_dir}", fg=typer.colors.GREEN)
264
+ project_dir = output_dir / f"{app_name}"
265
+ console.print(f"✅ Project created at {project_dir}")
266
+
267
+ @app.command()
268
+ def preprocess(
269
+ schema_path: Path = typer.Option(None, "--schema", "-s", help="Path to the OpenAPI schema file."),
270
+ output_path: Path = typer.Option(None, "--output", "-o", help="Path to save the processed schema."),
271
+ ):
272
+ from universal_mcp.utils.openapi.preprocessor import run_preprocessing
273
+ """Preprocess an OpenAPI schema using LLM to fill or enhance descriptions."""
274
+ run_preprocessing(schema_path, output_path)
275
275
 
276
276
 
277
277
  if __name__ == "__main__":
universal_mcp/config.py CHANGED
@@ -1,32 +1,105 @@
1
- from typing import Literal
1
+ from pathlib import Path
2
+ from typing import Any, Literal
2
3
 
3
- from pydantic import BaseModel
4
+ from pydantic import BaseModel, Field, SecretStr, field_validator
5
+ from pydantic_settings import BaseSettings, SettingsConfigDict
4
6
 
5
7
 
6
8
  class StoreConfig(BaseModel):
7
- name: str = "universal_mcp"
8
- type: Literal["memory", "environment", "keyring", "agentr"]
9
+ """Configuration for credential storage."""
10
+
11
+ name: str = Field(default="universal_mcp", description="Name of the store")
12
+ type: Literal["memory", "environment", "keyring", "agentr"] = Field(
13
+ default="memory", description="Type of credential storage to use"
14
+ )
15
+ path: Path | None = Field(
16
+ default=None, description="Path to store credentials (if applicable)"
17
+ )
9
18
 
10
19
 
11
20
  class IntegrationConfig(BaseModel):
12
- name: str
13
- type: Literal["api_key", "oauth", "agentr", "oauth2"]
14
- credentials: dict | None = None
15
- store: StoreConfig | None = None
21
+ """Configuration for API integrations."""
22
+
23
+ name: str = Field(..., description="Name of the integration")
24
+ type: Literal["api_key", "oauth", "agentr", "oauth2"] = Field(
25
+ default="api_key", description="Type of authentication to use"
26
+ )
27
+ credentials: dict[str, Any] | None = Field(
28
+ default=None, description="Integration-specific credentials"
29
+ )
30
+ store: StoreConfig | None = Field(
31
+ default=None, description="Store configuration for credentials"
32
+ )
16
33
 
17
34
 
18
35
  class AppConfig(BaseModel):
19
- name: str
20
- integration: IntegrationConfig | None = None
21
- actions: list[str] | None = None
22
-
23
-
24
- class ServerConfig(BaseModel):
25
- name: str = "Universal MCP"
26
- description: str = "Universal MCP"
27
- api_key: str | None = None
28
- type: Literal["local", "agentr"] = "agentr"
29
- transport: Literal["stdio", "sse", "http"] = "stdio"
30
- port: int = 8005
31
- apps: list[AppConfig] | None = None
32
- store: StoreConfig | None = None
36
+ """Configuration for individual applications."""
37
+
38
+ name: str = Field(..., description="Name of the application")
39
+ integration: IntegrationConfig | None = Field(
40
+ default=None, description="Integration configuration"
41
+ )
42
+ actions: list[str] | None = Field(
43
+ default=None, description="List of available actions"
44
+ )
45
+
46
+
47
+ class ServerConfig(BaseSettings):
48
+ """Main server configuration."""
49
+
50
+ model_config = SettingsConfigDict(
51
+ env_prefix="MCP_",
52
+ env_file=".env",
53
+ env_file_encoding="utf-8",
54
+ case_sensitive=True,
55
+ extra="allow",
56
+ )
57
+
58
+ name: str = Field(default="Universal MCP", description="Name of the MCP server")
59
+ description: str = Field(
60
+ default="Universal MCP", description="Description of the MCP server"
61
+ )
62
+ api_key: SecretStr | None = Field(
63
+ default=None, description="API key for authentication"
64
+ )
65
+ type: Literal["local", "agentr"] = Field(
66
+ default="agentr", description="Type of server deployment"
67
+ )
68
+ transport: Literal["stdio", "sse", "http"] = Field(
69
+ default="stdio", description="Transport protocol to use"
70
+ )
71
+ port: int = Field(
72
+ default=8005, description="Port to run the server on (if applicable)"
73
+ )
74
+ host: str = Field(
75
+ default="localhost", description="Host to bind the server to (if applicable)"
76
+ )
77
+ apps: list[AppConfig] | None = Field(
78
+ default=None, description="List of configured applications"
79
+ )
80
+ store: StoreConfig | None = Field(
81
+ default=None, description="Default store configuration"
82
+ )
83
+ debug: bool = Field(default=False, description="Enable debug mode")
84
+ log_level: str = Field(default="INFO", description="Logging level")
85
+ max_connections: int = Field(
86
+ default=100, description="Maximum number of concurrent connections"
87
+ )
88
+ request_timeout: int = Field(
89
+ default=60, description="Default request timeout in seconds"
90
+ )
91
+
92
+ @field_validator("log_level", mode="before")
93
+ def validate_log_level(cls, v: str) -> str:
94
+ valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
95
+ if v.upper() not in valid_levels:
96
+ raise ValueError(
97
+ f"Invalid log level. Must be one of: {', '.join(valid_levels)}"
98
+ )
99
+ return v.upper()
100
+
101
+ @field_validator("port", mode="before")
102
+ def validate_port(cls, v: int) -> int:
103
+ if not 1 <= v <= 65535:
104
+ raise ValueError("Port must be between 1 and 65535")
105
+ return v
@@ -11,3 +11,11 @@ class ToolError(Exception):
11
11
 
12
12
  class InvalidSignature(Exception):
13
13
  """Raised when a signature is invalid."""
14
+
15
+
16
+ class StoreError(Exception):
17
+ """Base exception class for store-related errors."""
18
+
19
+
20
+ class KeyNotFoundError(StoreError):
21
+ """Exception raised when a key is not found in the store."""
@@ -6,7 +6,7 @@ from loguru import logger
6
6
 
7
7
  from universal_mcp.exceptions import NotAuthorizedError
8
8
  from universal_mcp.stores import BaseStore
9
- from universal_mcp.stores.store import KeyNotFoundError
9
+ from universal_mcp.stores.store import KeyNotFoundError, MemoryStore
10
10
  from universal_mcp.utils.agentr import AgentrClient
11
11
 
12
12
 
@@ -91,7 +91,7 @@ class ApiKeyIntegration(Integration):
91
91
  store: Store instance for persisting credentials and other data
92
92
  """
93
93
 
94
- def __init__(self, name: str, store: BaseStore | None = None, **kwargs):
94
+ def __init__(self, name: str, store: BaseStore = MemoryStore(), **kwargs):
95
95
  self.type = "api_key"
96
96
  sanitized_name = sanitize_api_key_name(name)
97
97
  super().__init__(sanitized_name, store, **kwargs)
@@ -109,6 +109,22 @@ class ApiKeyIntegration(Integration):
109
109
  raise NotAuthorizedError(action) from e
110
110
  return self._api_key
111
111
 
112
+ @api_key.setter
113
+ def api_key(self, value: str | None) -> None:
114
+ """Set the API key.
115
+
116
+ Args:
117
+ value: The API key value to set.
118
+
119
+ Raises:
120
+ ValueError: If the API key is invalid.
121
+ """
122
+ if value is not None and not isinstance(value, str):
123
+ raise ValueError("API key must be a string")
124
+ self._api_key = value
125
+ if value is not None:
126
+ self.store.set(self.name, value)
127
+
112
128
  def get_credentials(self) -> dict[str, str]:
113
129
  """Get API key credentials.
114
130
 
universal_mcp/logger.py CHANGED
@@ -1,17 +1,68 @@
1
1
  import sys
2
+ from datetime import datetime
3
+ from pathlib import Path
2
4
 
3
5
  from loguru import logger
4
6
 
5
7
 
6
- def setup_logger():
8
+ def setup_logger(
9
+ log_file: Path | None = None,
10
+ rotation: str = "10 MB",
11
+ retention: str = "1 week",
12
+ compression: str = "zip",
13
+ level: str = "INFO",
14
+ format: str = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
15
+ ) -> None:
16
+ """Setup the logger with both stderr and optional file logging with rotation.
17
+
18
+ Args:
19
+ log_file: Optional path to log file. If None, only stderr logging is enabled.
20
+ rotation: When to rotate the log file. Can be size (e.g., "10 MB") or time (e.g., "1 day").
21
+ retention: How long to keep rotated log files (e.g., "1 week").
22
+ compression: Compression format for rotated logs ("zip", "gz", or None).
23
+ level: Minimum logging level.
24
+ format: Log message format string.
25
+ """
26
+ # Remove default handler
7
27
  logger.remove()
8
- # STDOUT cant be used as a sink because it will break the stream
9
- # logger.add(
10
- # sink=sys.stdout,
11
- # level="INFO",
12
- # )
13
- # STDERR
28
+
29
+ # Add stderr handler
14
30
  logger.add(
15
31
  sink=sys.stderr,
16
- level="INFO",
32
+ level=level,
33
+ format=format,
34
+ enqueue=True,
35
+ backtrace=True,
36
+ diagnose=True,
17
37
  )
38
+
39
+ # Add file handler if log_file is specified
40
+ if log_file:
41
+ # Ensure log directory exists
42
+ log_file.parent.mkdir(parents=True, exist_ok=True)
43
+
44
+ logger.add(
45
+ sink=str(log_file),
46
+ rotation=rotation,
47
+ retention=retention,
48
+ compression=compression,
49
+ level=level,
50
+ format=format,
51
+ enqueue=True,
52
+ backtrace=True,
53
+ diagnose=True,
54
+ )
55
+
56
+
57
+ def get_log_file_path(app_name: str) -> Path:
58
+ """Get a standardized log file path for an application.
59
+
60
+ Args:
61
+ app_name: Name of the application.
62
+
63
+ Returns:
64
+ Path to the log file in the format: logs/{app_name}/{app_name}_{date}.log
65
+ """
66
+ date_str = datetime.now().strftime("%Y%m%d")
67
+ log_dir = Path("logs") / app_name
68
+ return log_dir / f"{app_name}_{date_str}.log"
@@ -1,5 +1,5 @@
1
1
  from universal_mcp.config import ServerConfig
2
- from universal_mcp.servers.server import AgentRServer, LocalServer
2
+ from universal_mcp.servers.server import AgentRServer, LocalServer, SingleMCPServer
3
3
 
4
4
 
5
5
  def server_from_config(config: ServerConfig):
@@ -12,4 +12,4 @@ def server_from_config(config: ServerConfig):
12
12
  raise ValueError(f"Unsupported server type: {config.type}")
13
13
 
14
14
 
15
- __all__ = [AgentRServer, LocalServer, server_from_config]
15
+ __all__ = [AgentRServer, LocalServer, SingleMCPServer, server_from_config]
@@ -5,17 +5,7 @@ from typing import Any
5
5
  import keyring
6
6
  from loguru import logger
7
7
 
8
-
9
- class StoreError(Exception):
10
- """Base exception class for store-related errors."""
11
-
12
- pass
13
-
14
-
15
- class KeyNotFoundError(StoreError):
16
- """Exception raised when a key is not found in the store."""
17
-
18
- pass
8
+ from universal_mcp.exceptions import KeyNotFoundError, StoreError
19
9
 
20
10
 
21
11
  class BaseStore(ABC):
@@ -84,7 +74,7 @@ class MemoryStore(BaseStore):
84
74
 
85
75
  def __init__(self):
86
76
  """Initialize an empty dictionary to store the data."""
87
- self.data: dict[str, str] = {}
77
+ self.data: dict[str, Any] = {}
88
78
 
89
79
  def get(self, key: str) -> Any:
90
80
  """
@@ -1,3 +1,15 @@
1
- from .tools import Tool, ToolManager
1
+ from .adapters import (
2
+ convert_tool_to_langchain_tool,
3
+ convert_tool_to_mcp_tool,
4
+ convert_tool_to_openai_tool,
5
+ )
6
+ from .manager import ToolManager
7
+ from .tools import Tool
2
8
 
3
- __all__ = ["Tool", "ToolManager"]
9
+ __all__ = [
10
+ "Tool",
11
+ "ToolManager",
12
+ "convert_tool_to_langchain_tool",
13
+ "convert_tool_to_openai_tool",
14
+ "convert_tool_to_mcp_tool",
15
+ ]
@@ -1,6 +1,16 @@
1
+ from enum import Enum
2
+
1
3
  from universal_mcp.tools.tools import Tool
2
4
 
3
5
 
6
+ class ToolFormat(str, Enum):
7
+ """Supported tool formats."""
8
+
9
+ MCP = "mcp"
10
+ LANGCHAIN = "langchain"
11
+ OPENAI = "openai"
12
+
13
+
4
14
  def convert_tool_to_mcp_tool(
5
15
  tool: Tool,
6
16
  ):
@@ -40,4 +50,19 @@ def convert_tool_to_langchain_tool(
40
50
  description=tool.description or "",
41
51
  coroutine=call_tool,
42
52
  response_format="content",
53
+ args_schema=tool.parameters,
43
54
  )
55
+
56
+
57
+ def convert_tool_to_openai_tool(
58
+ tool: Tool,
59
+ ):
60
+ """Convert a Tool object to an OpenAI function."""
61
+ return {
62
+ "type": "function",
63
+ "function": {
64
+ "name": tool.name,
65
+ "description": tool.description,
66
+ "parameters": tool.parameters,
67
+ },
68
+ }
@@ -61,7 +61,7 @@ class ArgModelBase(BaseModel):
61
61
  That is, sub-models etc are not dumped - they are kept as pydantic models.
62
62
  """
63
63
  kwargs: dict[str, Any] = {}
64
- for field_name in self.model_fields:
64
+ for field_name in self.__class__.model_fields:
65
65
  kwargs[field_name] = getattr(self, field_name)
66
66
  return kwargs
67
67
 
@@ -82,6 +82,7 @@ class FuncMetadata(BaseModel):
82
82
  fn_is_async: bool,
83
83
  arguments_to_validate: dict[str, Any],
84
84
  arguments_to_pass_directly: dict[str, Any] | None,
85
+ context: dict[str, Any] | None = None,
85
86
  ) -> Any:
86
87
  """Call the given function with arguments validated and injected.
87
88
 
@@ -137,7 +138,10 @@ class FuncMetadata(BaseModel):
137
138
 
138
139
  @classmethod
139
140
  def func_metadata(
140
- cls, func: Callable[..., Any], skip_names: Sequence[str] = ()
141
+ cls,
142
+ func: Callable[..., Any],
143
+ skip_names: Sequence[str] = (),
144
+ arg_description: dict[str, str] | None = None,
141
145
  ) -> "FuncMetadata":
142
146
  """Given a function, return metadata including a pydantic model representing its
143
147
  signature.
@@ -198,6 +202,12 @@ class FuncMetadata(BaseModel):
198
202
  if param.default is not inspect.Parameter.empty
199
203
  else PydanticUndefined,
200
204
  )
205
+ if (
206
+ not field_info.title
207
+ and arg_description
208
+ and arg_description.get(param.name)
209
+ ):
210
+ field_info.title = arg_description.get(param.name)
201
211
  dynamic_pydantic_model_params[param.name] = (
202
212
  field_info.annotation,
203
213
  field_info,