8
8
import httpx
9
9
import os
10
10
import re
11
+ import tempfile
12
+ import subprocess
13
+ import ast
11
14
12
15
13
16
def sanitize_name (name : str ) -> str :
@@ -25,23 +28,110 @@ def sanitize_name(name: str) -> str:
25
28
26
29
27
30
async def get_mcp_servers () -> list [StdioServerParameters ]:
28
- async with httpx .AsyncClient () as client :
29
- """Get MCP servers from the API."""
30
- headers = {"Authorization" : f"Bearer { os .environ ['METATOOL_API_KEY' ]} " }
31
- response = await client .get (
32
- f"{ METATOOL_API_BASE_URL } /api/mcp-servers" , headers = headers
33
- )
34
- response .raise_for_status ()
35
- data = response .json ()
36
- server_params = []
37
- for params in data :
38
- # Convert empty lists and dicts to None
39
- if "args" in params and not params ["args" ]:
40
- params ["args" ] = None
41
- if "env" in params and not params ["env" ]:
42
- params ["env" ] = None
43
- server_params .append (StdioServerParameters (** params ))
44
- return server_params
31
+ try :
32
+ async with httpx .AsyncClient () as client :
33
+ """Get MCP servers from the API."""
34
+ headers = {"Authorization" : f"Bearer { os .environ ['METATOOL_API_KEY' ]} " }
35
+ response = await client .get (
36
+ f"{ METATOOL_API_BASE_URL } /api/mcp-servers" , headers = headers
37
+ )
38
+ response .raise_for_status ()
39
+ data = response .json ()
40
+ server_params = []
41
+ for params in data :
42
+ # Convert empty lists and dicts to None
43
+ if "args" in params and not params ["args" ]:
44
+ params ["args" ] = None
45
+ if "env" in params and not params ["env" ]:
46
+ params ["env" ] = None
47
+ server_params .append (StdioServerParameters (** params ))
48
+ return server_params
49
+ except Exception :
50
+ return []
51
+
52
+
53
+ def extract_imports (code : str ) -> list [str ]:
54
+ """Extract top-level import statements from the Python code."""
55
+ try :
56
+ tree = ast .parse (code )
57
+ imports = set ()
58
+
59
+ for node in ast .walk (tree ):
60
+ if isinstance (node , ast .Import ):
61
+ for alias in node .names :
62
+ imports .add (alias .name .split ("." )[0 ])
63
+ elif isinstance (node , ast .ImportFrom ) and node .module :
64
+ imports .add (node .module .split ("." )[0 ])
65
+
66
+ return list (imports )
67
+ except Exception as e :
68
+ raise RuntimeError (f"Error parsing imports: { e } " ) from e
69
+
70
+
71
+ def install_dependencies (dependencies : list [str ]):
72
+ """Install required dependencies using uv pip."""
73
+ try :
74
+ subprocess .run (["uv" , "pip" , "install" ] + dependencies , check = True )
75
+ except subprocess .CalledProcessError as e :
76
+ raise RuntimeError (f"Failed to install dependencies: { e } " ) from e
77
+
78
+
79
+ async def get_custom_mcp_servers () -> list [StdioServerParameters ]:
80
+ try :
81
+ async with httpx .AsyncClient () as client :
82
+ headers = {"Authorization" : f"Bearer { os .environ ['METATOOL_API_KEY' ]} " }
83
+ response = await client .get (
84
+ f"{ METATOOL_API_BASE_URL } /api/custom-mcp-servers" , headers = headers
85
+ )
86
+ response .raise_for_status ()
87
+ data = response .json ()
88
+ server_params = []
89
+
90
+ for params in data :
91
+ if "code" not in params or "code_uuid" not in params :
92
+ continue
93
+
94
+ code_uuid = params ["code_uuid" ]
95
+
96
+ # Create temp file for the script
97
+ with tempfile .NamedTemporaryFile (
98
+ mode = "w" , suffix = f"_{ code_uuid } .py" , delete = False
99
+ ) as temp_file :
100
+ temp_file .write (params ["code" ])
101
+ script_path = temp_file .name
102
+
103
+ # Extract dependencies from the code
104
+ try :
105
+ dependencies = extract_imports (params ["code" ])
106
+ if dependencies :
107
+ try :
108
+ install_dependencies (dependencies )
109
+ except Exception as e :
110
+ print (
111
+ f"Failed to install dependencies for server { code_uuid } : { e } "
112
+ )
113
+ continue
114
+ except Exception as e :
115
+ print (f"Failed to extract imports for server { code_uuid } : { e } " )
116
+ continue
117
+
118
+ params ["command" ] = "uv"
119
+ params ["args" ] = ["run" , script_path ] + params .get ("additionalArgs" , [])
120
+
121
+ if "env" in params and not params ["env" ]:
122
+ params ["env" ] = None
123
+
124
+ server_params .append (StdioServerParameters (** params ))
125
+ return server_params
126
+ except Exception as e :
127
+ print (f"Error fetching MCP servers: { e } " )
128
+ return []
129
+
130
+
131
+ async def get_all_mcp_servers () -> list [StdioServerParameters ]:
132
+ server_params = await get_mcp_servers ()
133
+ custom_server_params = await get_custom_mcp_servers ()
134
+ return server_params + custom_server_params
45
135
46
136
47
137
async def initialize_session (session : ClientSession ) -> dict :
@@ -57,7 +147,7 @@ async def initialize_session(session: ClientSession) -> dict:
57
147
@server .list_tools ()
58
148
async def handle_list_tools () -> list [types .Tool ]:
59
149
# Reload MCP servers
60
- remote_server_params = await get_mcp_servers ()
150
+ remote_server_params = await get_all_mcp_servers ()
61
151
62
152
# Combine with default servers
63
153
all_server_params = remote_server_params
@@ -94,7 +184,7 @@ async def handle_call_tool(
94
184
)
95
185
96
186
# Get all server parameters
97
- remote_server_params = await get_mcp_servers ()
187
+ remote_server_params = await get_all_mcp_servers ()
98
188
99
189
# Find the matching server parameters
100
190
for params in remote_server_params :
0 commit comments