utim-cli 1.0.0__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.
- utim_cli/__init__.py +40 -0
- utim_cli/agent.py +359 -0
- utim_cli/auth.py +208 -0
- utim_cli/backup.py +101 -0
- utim_cli/billing.py +40 -0
- utim_cli/blender_agent.py +1018 -0
- utim_cli/bootstrap.py +324 -0
- utim_cli/client_utils.py +135 -0
- utim_cli/config.py +194 -0
- utim_cli/context_pruner.py +504 -0
- utim_cli/doctor.py +118 -0
- utim_cli/knowledge_graph.py +462 -0
- utim_cli/logger.py +121 -0
- utim_cli/mcp_clean_wrapper.py +55 -0
- utim_cli/mcp_client.py +198 -0
- utim_cli/mcp_registry.json +1102 -0
- utim_cli/orchestrator.py +3209 -0
- utim_cli/reflection.py +200 -0
- utim_cli/report.py +100 -0
- utim_cli/scrapy_search.py +229 -0
- utim_cli/share.py +320 -0
- utim_cli/share_tui.py +554 -0
- utim_cli/situational_scoring.py +269 -0
- utim_cli/state.py +15 -0
- utim_cli/tools.py +3381 -0
- utim_cli/utim.py +4051 -0
- utim_cli/vector_memory.py +629 -0
- utim_cli/workspace.py +33 -0
- utim_cli-1.0.0.dist-info/METADATA +134 -0
- utim_cli-1.0.0.dist-info/RECORD +34 -0
- utim_cli-1.0.0.dist-info/WHEEL +5 -0
- utim_cli-1.0.0.dist-info/entry_points.txt +2 -0
- utim_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- utim_cli-1.0.0.dist-info/top_level.txt +1 -0
utim_cli/mcp_client.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import asyncio
|
|
4
|
+
import threading
|
|
5
|
+
from typing import Dict, List, Any, Optional
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from mcp import ClientSession, StdioServerParameters
|
|
9
|
+
from mcp.client.stdio import stdio_client
|
|
10
|
+
except ImportError:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class MCPManager:
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.sessions: Dict[str, ClientSession] = {}
|
|
16
|
+
self._exit_stacks = {}
|
|
17
|
+
self._tool_to_server: Dict[str, str] = {}
|
|
18
|
+
self._started = False
|
|
19
|
+
self.cached_tools: List[Dict[str, Any]] = []
|
|
20
|
+
self.server_tools: Dict[str, List[str]] = {} # server_name -> list of tool names
|
|
21
|
+
self._loop = None
|
|
22
|
+
self._thread = None
|
|
23
|
+
|
|
24
|
+
def start_loop(self):
|
|
25
|
+
if self._loop is None:
|
|
26
|
+
self._loop = asyncio.new_event_loop()
|
|
27
|
+
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
|
28
|
+
self._thread.start()
|
|
29
|
+
|
|
30
|
+
def _run_loop(self):
|
|
31
|
+
asyncio.set_event_loop(self._loop)
|
|
32
|
+
self._loop.run_forever()
|
|
33
|
+
|
|
34
|
+
def run_coro(self, coro):
|
|
35
|
+
self.start_loop()
|
|
36
|
+
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
|
|
37
|
+
return future.result()
|
|
38
|
+
|
|
39
|
+
def start(self):
|
|
40
|
+
if self._started:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Check if mcp library is available
|
|
44
|
+
try:
|
|
45
|
+
from mcp import ClientSession, StdioServerParameters
|
|
46
|
+
from mcp.client.stdio import stdio_client
|
|
47
|
+
except ImportError:
|
|
48
|
+
# Create config if it doesn't exist, just in case
|
|
49
|
+
config_path = os.path.abspath(os.path.join(".utim", "mcp.json"))
|
|
50
|
+
if not os.path.exists(config_path):
|
|
51
|
+
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
|
52
|
+
with open(config_path, "w") as f:
|
|
53
|
+
json.dump({"mcpServers": {}}, f, indent=2)
|
|
54
|
+
self._started = True
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
self.start_loop()
|
|
58
|
+
self.run_coro(self._start_async())
|
|
59
|
+
|
|
60
|
+
async def _start_async(self):
|
|
61
|
+
config_path = os.path.abspath(os.path.join(".utim", "mcp.json"))
|
|
62
|
+
if not os.path.exists(config_path):
|
|
63
|
+
# Create dummy config
|
|
64
|
+
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
|
65
|
+
with open(config_path, "w") as f:
|
|
66
|
+
json.dump({"mcpServers": {}}, f, indent=2)
|
|
67
|
+
self._started = True
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
with open(config_path, "r") as f:
|
|
72
|
+
config = json.load(f)
|
|
73
|
+
except Exception:
|
|
74
|
+
self._started = True
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
servers = config.get("mcpServers", {})
|
|
78
|
+
|
|
79
|
+
for name, srv_config in servers.items():
|
|
80
|
+
command = srv_config.get("command")
|
|
81
|
+
args = srv_config.get("args", [])
|
|
82
|
+
env = srv_config.get("env", {})
|
|
83
|
+
|
|
84
|
+
if not command:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# Strip outer quotes from argument strings if they exist (common config error for space-containing paths)
|
|
88
|
+
cleaned_args = []
|
|
89
|
+
for arg in args:
|
|
90
|
+
if isinstance(arg, str):
|
|
91
|
+
if (arg.startswith('"') and arg.endswith('"')) or (arg.startswith("'") and arg.endswith("'")):
|
|
92
|
+
arg = arg[1:-1]
|
|
93
|
+
cleaned_args.append(arg)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# Merge current env
|
|
97
|
+
merged_env = os.environ.copy()
|
|
98
|
+
merged_env.update(env)
|
|
99
|
+
|
|
100
|
+
# Setup StdioServerParameters with stdout wrapper to prevent JSON-RPC pollution
|
|
101
|
+
import sys
|
|
102
|
+
import shutil
|
|
103
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
104
|
+
wrapper_path = os.path.join(current_dir, "mcp_clean_wrapper.py")
|
|
105
|
+
resolved_command = shutil.which(command) or command
|
|
106
|
+
|
|
107
|
+
server_params = StdioServerParameters(
|
|
108
|
+
command=sys.executable,
|
|
109
|
+
args=["-u", wrapper_path, resolved_command] + cleaned_args,
|
|
110
|
+
env=merged_env
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
from contextlib import AsyncExitStack
|
|
114
|
+
stack = AsyncExitStack()
|
|
115
|
+
self._exit_stacks[name] = stack
|
|
116
|
+
|
|
117
|
+
read, write = await stack.enter_async_context(stdio_client(server_params))
|
|
118
|
+
session = await stack.enter_async_context(ClientSession(read, write))
|
|
119
|
+
await session.initialize()
|
|
120
|
+
|
|
121
|
+
self.sessions[name] = session
|
|
122
|
+
|
|
123
|
+
# Retrieve tools from the server and cache them
|
|
124
|
+
try:
|
|
125
|
+
res = await session.list_tools()
|
|
126
|
+
self.server_tools[name] = []
|
|
127
|
+
for t in res.tools:
|
|
128
|
+
full_name = f"{name}__{t.name}"
|
|
129
|
+
self._tool_to_server[full_name] = name
|
|
130
|
+
self.server_tools[name].append(t.name)
|
|
131
|
+
|
|
132
|
+
# Convert MCP tool schema to OpenAI/OpenRouter format
|
|
133
|
+
self.cached_tools.append({
|
|
134
|
+
"type": "function",
|
|
135
|
+
"function": {
|
|
136
|
+
"name": full_name,
|
|
137
|
+
"description": f"[{name}] {t.description}",
|
|
138
|
+
"parameters": t.inputSchema
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
except Exception as e:
|
|
142
|
+
print(f"Error fetching tools from MCP server {name}: {e}")
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
import traceback
|
|
146
|
+
print(f"Error starting MCP server {name}: {e}")
|
|
147
|
+
|
|
148
|
+
self._started = True
|
|
149
|
+
|
|
150
|
+
def get_tools(self) -> List[Dict[str, Any]]:
|
|
151
|
+
"""Synchronously returns the cached list of MCP tools."""
|
|
152
|
+
return self.cached_tools
|
|
153
|
+
|
|
154
|
+
def get_notification_context(self) -> str:
|
|
155
|
+
"""Returns the context notification string for connected MCP servers."""
|
|
156
|
+
notifications = []
|
|
157
|
+
for server_name, tool_names in self.server_tools.items():
|
|
158
|
+
if tool_names:
|
|
159
|
+
tools_str = ", ".join(tool_names)
|
|
160
|
+
notifications.append(f"this mcp server {server_name} is connected and a new set of tools are available for u: {tools_str}")
|
|
161
|
+
return "\n".join(notifications)
|
|
162
|
+
|
|
163
|
+
def call_tool(self, server_name: str, tool_name: str, arguments: Dict[str, Any]) -> str:
|
|
164
|
+
"""Synchronously routes tool call execution to the background thread."""
|
|
165
|
+
return self.run_coro(self._call_tool_async(server_name, tool_name, arguments))
|
|
166
|
+
|
|
167
|
+
async def _call_tool_async(self, server_name: str, tool_name: str, arguments: Dict[str, Any]) -> str:
|
|
168
|
+
if server_name not in self.sessions:
|
|
169
|
+
return f"Error: MCP server '{server_name}' not running."
|
|
170
|
+
|
|
171
|
+
session = self.sessions[server_name]
|
|
172
|
+
try:
|
|
173
|
+
res = await session.call_tool(tool_name, arguments)
|
|
174
|
+
if getattr(res, "isError", False):
|
|
175
|
+
return f"Error from {server_name}: " + "\n".join(c.text for c in res.content)
|
|
176
|
+
return "\n".join(c.text for c in res.content)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
return f"Error calling {tool_name} on {server_name}: {str(e)}"
|
|
179
|
+
|
|
180
|
+
def restart(self):
|
|
181
|
+
"""Synchronously restarts and reloads all MCP sessions."""
|
|
182
|
+
self.run_coro(self._restart_async())
|
|
183
|
+
|
|
184
|
+
async def _restart_async(self):
|
|
185
|
+
for name, stack in list(self._exit_stacks.items()):
|
|
186
|
+
try:
|
|
187
|
+
await stack.aclose()
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
self.sessions.clear()
|
|
191
|
+
self._exit_stacks.clear()
|
|
192
|
+
self._tool_to_server.clear()
|
|
193
|
+
self.cached_tools.clear()
|
|
194
|
+
self.server_tools.clear()
|
|
195
|
+
self._started = False
|
|
196
|
+
await self._start_async()
|
|
197
|
+
|
|
198
|
+
mcp_manager = MCPManager()
|