utim-cli 1.47.6__tar.gz → 1.47.8__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.
Files changed (89) hide show
  1. {utim_cli-1.47.6 → utim_cli-1.47.8}/CHANGELOG.md +11 -0
  2. {utim_cli-1.47.6/utim_cli.egg-info → utim_cli-1.47.8}/PKG-INFO +1 -1
  3. {utim_cli-1.47.6 → utim_cli-1.47.8}/pyproject.toml +2 -2
  4. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/__init__.py +1 -1
  5. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/_version.py +1 -1
  6. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/bootstrap.py +1 -0
  7. utim_cli-1.47.8/utim_cli/mcp_client.py +343 -0
  8. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/reflection.py +500 -202
  9. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/utim.py +101 -9
  10. {utim_cli-1.47.6 → utim_cli-1.47.8/utim_cli.egg-info}/PKG-INFO +1 -1
  11. utim_cli-1.47.8/utim_cli.egg-info/entry_points.txt +2 -0
  12. utim_cli-1.47.6/utim_cli/mcp_client.py +0 -210
  13. utim_cli-1.47.6/utim_cli.egg-info/entry_points.txt +0 -2
  14. {utim_cli-1.47.6 → utim_cli-1.47.8}/LICENSE +0 -0
  15. {utim_cli-1.47.6 → utim_cli-1.47.8}/MANIFEST.in +0 -0
  16. {utim_cli-1.47.6 → utim_cli-1.47.8}/README.md +0 -0
  17. {utim_cli-1.47.6 → utim_cli-1.47.8}/setup.cfg +0 -0
  18. {utim_cli-1.47.6 → utim_cli-1.47.8}/setup.py +0 -0
  19. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/agent.py +0 -0
  20. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/auth.py +0 -0
  21. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/backup.py +0 -0
  22. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/blender_agent.py +0 -0
  23. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/client_utils.py +0 -0
  24. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/config.py +0 -0
  25. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/constants.py +0 -0
  26. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/context_pruner.py +0 -0
  27. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/doctor.py +0 -0
  28. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/knowledge_graph.py +0 -0
  29. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/local_db.py +0 -0
  30. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/logger.py +0 -0
  31. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/mcp_clean_wrapper.py +0 -0
  32. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/mcp_registry.json +0 -0
  33. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/orchestrator.py +0 -0
  34. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/report.py +0 -0
  35. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/scrapy_search.py +0 -0
  36. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/__init__.py +0 -0
  37. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/auth.py +0 -0
  38. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/db.py +0 -0
  39. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/docs_md/about.md +0 -0
  40. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/docs_md/changelog.md +0 -0
  41. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/docs_md/docs.md +0 -0
  42. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/docs_md/features.md +0 -0
  43. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/docs_md/license.md +0 -0
  44. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/docs_md/pricing.md +0 -0
  45. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/docs_md/privacy.md +0 -0
  46. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/docs_md/refund.md +0 -0
  47. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/docs_md/support.md +0 -0
  48. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/docs_md/terms.md +0 -0
  49. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/email_utils.py +0 -0
  50. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/exchange_rate.py +0 -0
  51. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/firebase.py +0 -0
  52. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/history.py +0 -0
  53. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/logging_config.py +0 -0
  54. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/models.py +0 -0
  55. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/pricing_updater.py +0 -0
  56. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/rate_limit.py +0 -0
  57. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/router.py +0 -0
  58. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/routes/__init__.py +0 -0
  59. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/routes/auth_routes.py +0 -0
  60. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/routes/completion_routes.py +0 -0
  61. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/routes/credit_routes.py +0 -0
  62. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/routes/feedback_routes.py +0 -0
  63. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/routes/quota_routes.py +0 -0
  64. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/routes/referral_routes.py +0 -0
  65. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/routes/session_routes.py +0 -0
  66. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/routes/share_routes.py +0 -0
  67. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/server/server.py +0 -0
  68. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/share.py +0 -0
  69. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/share_tui.py +0 -0
  70. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/situational_scoring.py +0 -0
  71. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/state.py +0 -0
  72. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/tools.py +0 -0
  73. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/tui/__init__.py +0 -0
  74. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/tui/feedback_dialog.py +0 -0
  75. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/tui/history_dialog.py +0 -0
  76. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/tui/mcp_dialog.py +0 -0
  77. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/tui/model_dialog.py +0 -0
  78. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/tui/quota_dialog.py +0 -0
  79. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/tui/resume_dialog.py +0 -0
  80. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/tui/skills_dialog.py +0 -0
  81. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/tui/tools_dialog.py +0 -0
  82. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/tui/update_dialog.py +0 -0
  83. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/utilities.py +0 -0
  84. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/vector_memory.py +0 -0
  85. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli/workspace.py +0 -0
  86. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli.egg-info/SOURCES.txt +0 -0
  87. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli.egg-info/dependency_links.txt +0 -0
  88. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli.egg-info/requires.txt +0 -0
  89. {utim_cli-1.47.6 → utim_cli-1.47.8}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.47.8] - 2026-07-20
4
+
5
+ ### Added
6
+ - **Android Termux Pure-Python Architecture**: Implemented pure-Python line-delimited JSON-RPC stdio client fallback (`PurePythonMCPSession`) for Model Context Protocol (MCP) servers, enabling 100% feature parity on Android Termux without requiring Rust compilation or `pydantic-core`.
7
+ - **Web Support Assistant Disclaimer & Installation Flow**: Added glassmorphism notice bar and 4-step installation visual guide on landing website to distinguish web support chat from local CLI agent.
8
+ - **Enhanced Cognitive Reflection Pipeline**: Replaced single-turn random skill creation with a 5-request interaction buffering engine. Analyzes full interaction trajectories across turns (detecting silent tool calling and file format assumption complaints like image PDFs vs text PDFs), with strict usefulness filtering to prevent storing junk data.
9
+ - **RAG-Based Skill Creation & Intelligent Hold-Back Architecture**: Implemented RAG skill synthesis over ChromaDB Vector Memory. Enforces a minimum threshold ($\ge 3$ accumulated experiences across sessions), an LLM usability/cohesion evaluation (`is_sufficiently_usable`), and cooldown stride tracking to hold back skill creation until technical patterns are mature and genuinely usable.
10
+
11
+ ### Fixed
12
+ - **Device Sign-In Unauthenticated Flow**: Fixed device authorization flow on `https://utim.dev/activate?code=XXXX-XXXX`. Auto-redirects unauthenticated users to `/auth` with a countdown while preserving the device code, bouncing them back to `/activate` automatically after sign-in to complete terminal authorization without broken loops.
13
+
3
14
  ## [1.47.6] - 2026-07-19
4
15
 
5
16
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 1.47.6
3
+ Version: 1.47.8
4
4
  Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://utim.dev
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "1.47.6"
7
+ version = "1.47.8"
8
8
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -79,7 +79,7 @@ legacy-agent = [
79
79
  ]
80
80
 
81
81
  [project.scripts]
82
- utim = "utim_cli.utim:app"
82
+ utim = "utim_cli.utim:main_cli_entry"
83
83
 
84
84
  [tool.setuptools.packages.find]
85
85
  include = ["utim_cli*"]
@@ -5,7 +5,7 @@ try:
5
5
  from utim_cli._version import VERSION as __version__
6
6
  except ImportError:
7
7
  # Fallback during first-install / editable-install edge cases
8
- __version__ = "1.47.6"
8
+ __version__ = "1.47.8"
9
9
 
10
10
  import builtins, os, pathlib, urllib.parse, re
11
11
 
@@ -11,4 +11,4 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "1.47.6"
14
+ VERSION = "1.47.8"
@@ -4,6 +4,7 @@ Auto-creates the .utim folder structure, custom skills, and local database on fi
4
4
  """
5
5
 
6
6
  import os
7
+ from pathlib import Path
7
8
  from utim_cli.config import get_utim_dir
8
9
 
9
10
  UTIM_DIR = get_utim_dir()
@@ -0,0 +1,343 @@
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
+ HAS_MCP_LIB = True
11
+ except ImportError:
12
+ HAS_MCP_LIB = False
13
+
14
+ # =========================================================================
15
+ # Pure-Python Stdio MCP Client Fallback (Zero C/Rust/pydantic-core dependency)
16
+ # =========================================================================
17
+ class PurePythonMCPTool:
18
+ def __init__(self, name: str, description: str, inputSchema: dict):
19
+ self.name = name
20
+ self.description = description
21
+ self.inputSchema = inputSchema
22
+
23
+ class PurePythonMCPContent:
24
+ def __init__(self, text: str):
25
+ self.text = text
26
+
27
+ class PurePythonMCPResult:
28
+ def __init__(self, content: list, isError: bool = False):
29
+ self.content = content
30
+ self.isError = isError
31
+
32
+ class PurePythonMCPSession:
33
+ """Pure-Python line-delimited JSON-RPC stdio client for MCP servers."""
34
+ def __init__(self, command: str, args: list, env: dict):
35
+ self.command = command
36
+ self.args = args
37
+ self.env = env
38
+ self.proc = None
39
+ self._req_id = 0
40
+
41
+ def _next_id(self):
42
+ self._req_id += 1
43
+ return self._req_id
44
+
45
+ async def start(self):
46
+ import sys
47
+ import shutil
48
+ current_dir = os.path.dirname(os.path.abspath(__file__))
49
+ wrapper_path = os.path.join(current_dir, "mcp_clean_wrapper.py")
50
+ resolved_command = shutil.which(self.command) or self.command
51
+
52
+ cmd_list = [sys.executable, "-u", wrapper_path, resolved_command] + self.args
53
+ self.proc = await asyncio.create_subprocess_exec(
54
+ *cmd_list,
55
+ stdin=asyncio.subprocess.PIPE,
56
+ stdout=asyncio.subprocess.PIPE,
57
+ stderr=asyncio.subprocess.PIPE,
58
+ env=self.env
59
+ )
60
+
61
+ async def _send_request(self, method: str, params: dict) -> dict:
62
+ if not self.proc or not self.proc.stdin:
63
+ raise RuntimeError("MCP process stdin not open")
64
+ req_id = self._next_id()
65
+ payload = json.dumps({
66
+ "jsonrpc": "2.0",
67
+ "id": req_id,
68
+ "method": method,
69
+ "params": params
70
+ }) + "\n"
71
+ self.proc.stdin.write(payload.encode("utf-8"))
72
+ await self.proc.stdin.drain()
73
+
74
+ while True:
75
+ line = await self.proc.stdout.readline()
76
+ if not line:
77
+ raise RuntimeError("MCP server process output ended unexpectedly.")
78
+ line_str = line.decode("utf-8", errors="ignore").strip()
79
+ if not line_str:
80
+ continue
81
+ try:
82
+ msg = json.loads(line_str)
83
+ if msg.get("id") == req_id:
84
+ if "error" in msg:
85
+ err_msg = msg["error"].get("message", "MCP RPC Error")
86
+ raise RuntimeError(err_msg)
87
+ return msg.get("result", {})
88
+ except json.JSONDecodeError:
89
+ continue
90
+
91
+ async def _send_notification(self, method: str, params: dict):
92
+ if not self.proc or not self.proc.stdin:
93
+ return
94
+ payload = json.dumps({
95
+ "jsonrpc": "2.0",
96
+ "method": method,
97
+ "params": params
98
+ }) + "\n"
99
+ self.proc.stdin.write(payload.encode("utf-8"))
100
+ await self.proc.stdin.drain()
101
+
102
+ async def initialize(self):
103
+ await self._send_request("initialize", {
104
+ "protocolVersion": "2024-11-05",
105
+ "capabilities": {},
106
+ "clientInfo": {"name": "utim", "version": "1.0"}
107
+ })
108
+ await self._send_notification("notifications/initialized", {})
109
+
110
+ async def list_tools(self):
111
+ res = await self._send_request("tools/list", {})
112
+ raw_tools = res.get("tools", [])
113
+ tools = []
114
+ for t in raw_tools:
115
+ tools.append(PurePythonMCPTool(
116
+ name=t.get("name", ""),
117
+ description=t.get("description", ""),
118
+ inputSchema=t.get("inputSchema", {})
119
+ ))
120
+ class ToolListResult:
121
+ def __init__(self, t_list):
122
+ self.tools = t_list
123
+ return ToolListResult(tools)
124
+
125
+ async def call_tool(self, name: str, arguments: dict):
126
+ res = await self._send_request("tools/call", {
127
+ "name": name,
128
+ "arguments": arguments
129
+ })
130
+ is_error = res.get("isError", False)
131
+ raw_content = res.get("content", [])
132
+ contents = []
133
+ for c in raw_content:
134
+ if isinstance(c, dict) and "text" in c:
135
+ contents.append(PurePythonMCPContent(c["text"]))
136
+ elif isinstance(c, str):
137
+ contents.append(PurePythonMCPContent(c))
138
+ if not contents:
139
+ contents.append(PurePythonMCPContent(json.dumps(res)))
140
+ return PurePythonMCPResult(contents, isError=is_error)
141
+
142
+ async def close(self):
143
+ if self.proc:
144
+ try:
145
+ self.proc.terminate()
146
+ await self.proc.wait()
147
+ except Exception:
148
+ pass
149
+
150
+
151
+ class MCPManager:
152
+ def __init__(self):
153
+ self.sessions: Dict[str, Any] = {}
154
+ self._exit_stacks = {}
155
+ self._tool_to_server: Dict[str, str] = {}
156
+ self._started = False
157
+ self.cached_tools: List[Dict[str, Any]] = []
158
+ self.server_tools: Dict[str, List[str]] = {} # server_name -> list of tool names
159
+ self._loop = None
160
+ self._thread = None
161
+
162
+ def start_loop(self):
163
+ if self._loop is None:
164
+ self._loop = asyncio.new_event_loop()
165
+ self._thread = threading.Thread(target=self._run_loop, daemon=True)
166
+ self._thread.start()
167
+
168
+ def _run_loop(self):
169
+ asyncio.set_event_loop(self._loop)
170
+ self._loop.run_forever()
171
+
172
+ def run_coro(self, coro):
173
+ self.start_loop()
174
+ future = asyncio.run_coroutine_threadsafe(coro, self._loop)
175
+ return future.result()
176
+
177
+ def start(self):
178
+ if self._started:
179
+ return
180
+
181
+ from utim_cli.config import get_utim_dir
182
+ config_path = os.path.abspath(os.path.join(get_utim_dir(), "mcp.json"))
183
+ if not os.path.exists(config_path):
184
+ os.makedirs(os.path.dirname(config_path), exist_ok=True)
185
+ with open(config_path, "w") as f:
186
+ json.dump({"mcpServers": {}}, f, indent=2)
187
+
188
+ self.start_loop()
189
+ self.run_coro(self._start_async())
190
+
191
+ async def _start_async(self):
192
+ from utim_cli.config import get_utim_dir
193
+ config_path = os.path.abspath(os.path.join(get_utim_dir(), "mcp.json"))
194
+ if not os.path.exists(config_path):
195
+ os.makedirs(os.path.dirname(config_path), exist_ok=True)
196
+ with open(config_path, "w") as f:
197
+ json.dump({"mcpServers": {}}, f, indent=2)
198
+ self._started = True
199
+ return
200
+
201
+ try:
202
+ with open(config_path, "r") as f:
203
+ config = json.load(f)
204
+ except Exception:
205
+ self._started = True
206
+ return
207
+
208
+ servers = config.get("mcpServers") or {}
209
+ if not isinstance(servers, dict):
210
+ servers = {}
211
+
212
+ for name, srv_config in servers.items():
213
+ command = srv_config.get("command")
214
+ args = srv_config.get("args", [])
215
+ env = srv_config.get("env", {})
216
+
217
+ if not command:
218
+ continue
219
+
220
+ cleaned_args = []
221
+ for arg in args:
222
+ if isinstance(arg, str):
223
+ if (arg.startswith('"') and arg.endswith('"')) or (arg.startswith("'") and arg.endswith("'")):
224
+ arg = arg[1:-1]
225
+ cleaned_args.append(arg)
226
+
227
+ try:
228
+ merged_env = os.environ.copy()
229
+ merged_env.update(env)
230
+
231
+ if HAS_MCP_LIB:
232
+ import sys
233
+ import shutil
234
+ current_dir = os.path.dirname(os.path.abspath(__file__))
235
+ wrapper_path = os.path.join(current_dir, "mcp_clean_wrapper.py")
236
+ resolved_command = shutil.which(command) or command
237
+
238
+ server_params = StdioServerParameters(
239
+ command=sys.executable,
240
+ args=["-u", wrapper_path, resolved_command] + cleaned_args,
241
+ env=merged_env
242
+ )
243
+
244
+ from contextlib import AsyncExitStack
245
+ stack = AsyncExitStack()
246
+ self._exit_stacks[name] = stack
247
+
248
+ read, write = await stack.enter_async_context(stdio_client(server_params))
249
+ session = await stack.enter_async_context(ClientSession(read, write))
250
+ await asyncio.wait_for(session.initialize(), timeout=60.0)
251
+ self.sessions[name] = session
252
+ else:
253
+ # Pure-Python stdio client fallback (Android Termux / minimal installs)
254
+ session = PurePythonMCPSession(command, cleaned_args, merged_env)
255
+ await session.start()
256
+ await asyncio.wait_for(session.initialize(), timeout=60.0)
257
+ self.sessions[name] = session
258
+
259
+ # Retrieve tools from the server and cache them
260
+ try:
261
+ res = await session.list_tools()
262
+ self.server_tools[name] = []
263
+ for t in res.tools:
264
+ full_name = f"{name}__{t.name}"
265
+ self._tool_to_server[full_name] = name
266
+ self.server_tools[name].append(t.name)
267
+
268
+ self.cached_tools.append({
269
+ "type": "function",
270
+ "function": {
271
+ "name": full_name,
272
+ "description": f"[{name}] {t.description}",
273
+ "parameters": t.inputSchema
274
+ }
275
+ })
276
+ except Exception as e:
277
+ print(f"Error fetching tools from MCP server {name}: {e}")
278
+
279
+ except Exception as e:
280
+ print(f"Error starting MCP server {name}: {e}")
281
+
282
+ self._started = True
283
+
284
+ def get_tools(self) -> List[Dict[str, Any]]:
285
+ """Synchronously returns the cached list of MCP tools."""
286
+ return self.cached_tools
287
+
288
+ def get_notification_context(self) -> str:
289
+ """Returns the context notification string for connected MCP servers."""
290
+ from utim_cli.config import config
291
+ disabled = config.get("disabled_tools") or []
292
+ if not isinstance(disabled, list):
293
+ disabled = []
294
+ notifications = []
295
+ for server_name, tool_names in self.server_tools.items():
296
+ enabled_tool_names = [t for t in tool_names if f"{server_name}__{t}" not in disabled]
297
+ if enabled_tool_names:
298
+ tools_str = ", ".join(enabled_tool_names)
299
+ notifications.append(f"this mcp server {server_name} is connected and a new set of tools are available for u: {tools_str}")
300
+ return "\n".join(notifications)
301
+
302
+ def call_tool(self, server_name: str, tool_name: str, arguments: Dict[str, Any]) -> str:
303
+ """Synchronously routes tool call execution to the background thread."""
304
+ return self.run_coro(self._call_tool_async(server_name, tool_name, arguments))
305
+
306
+ async def _call_tool_async(self, server_name: str, tool_name: str, arguments: Dict[str, Any]) -> str:
307
+ if server_name not in self.sessions:
308
+ return f"Error: MCP server '{server_name}' not running."
309
+
310
+ session = self.sessions[server_name]
311
+ try:
312
+ res = await session.call_tool(tool_name, arguments)
313
+ if getattr(res, "isError", False):
314
+ return f"Error from {server_name}: " + "\n".join(c.text for c in res.content)
315
+ return "\n".join(c.text for c in res.content)
316
+ except Exception as e:
317
+ return f"Error calling {tool_name} on {server_name}: {str(e)}"
318
+
319
+ def restart(self):
320
+ """Synchronously restarts and reloads all MCP sessions."""
321
+ self.run_coro(self._restart_async())
322
+
323
+ async def _restart_async(self):
324
+ for name, stack in list(self._exit_stacks.items()):
325
+ try:
326
+ await stack.aclose()
327
+ except Exception:
328
+ pass
329
+ for name, session in list(self.sessions.items()):
330
+ if hasattr(session, "close"):
331
+ try:
332
+ await session.close()
333
+ except Exception:
334
+ pass
335
+ self.sessions.clear()
336
+ self._exit_stacks.clear()
337
+ self._tool_to_server.clear()
338
+ self.cached_tools.clear()
339
+ self.server_tools.clear()
340
+ self._started = False
341
+ await self._start_async()
342
+
343
+ mcp_manager = MCPManager()