Skip to content

Commit d7bae57

Browse files
authored
Merge pull request #4 from metatool-ai/custom-mcp-servers
Allow read from custom mcp servers from MetaTool App
2 parents 9a9c449 + d84d46f commit d7bae57

File tree

4 files changed

+112
-22
lines changed

4 files changed

+112
-22
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "mcp-server-metatool"
7-
version = "0.0.2"
7+
version = "0.0.3"
88
description = "Metatool MCP Server"
99
readme = "README.md"
1010
requires-python = ">=3.10"

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup(
44
name="mcp-server-metatool",
5-
version="0.0.2",
5+
version="0.0.3",
66
author="James Zhang",
77
author_email="[email protected]",
88
description="Metatool MCP Server",

src/mcp_server_metatool/server.py

Lines changed: 109 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import httpx
99
import os
1010
import re
11+
import tempfile
12+
import subprocess
13+
import ast
1114

1215

1316
def sanitize_name(name: str) -> str:
@@ -25,23 +28,110 @@ def sanitize_name(name: str) -> str:
2528

2629

2730
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
45135

46136

47137
async def initialize_session(session: ClientSession) -> dict:
@@ -57,7 +147,7 @@ async def initialize_session(session: ClientSession) -> dict:
57147
@server.list_tools()
58148
async def handle_list_tools() -> list[types.Tool]:
59149
# Reload MCP servers
60-
remote_server_params = await get_mcp_servers()
150+
remote_server_params = await get_all_mcp_servers()
61151

62152
# Combine with default servers
63153
all_server_params = remote_server_params
@@ -94,7 +184,7 @@ async def handle_call_tool(
94184
)
95185

96186
# Get all server parameters
97-
remote_server_params = await get_mcp_servers()
187+
remote_server_params = await get_all_mcp_servers()
98188

99189
# Find the matching server parameters
100190
for params in remote_server_params:

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)