mcp polishing

frdel committed Jun 4, 2025 at 17:03 UTC 08f69c10126f958a03cf6608ed6bb6b2b8a4d282
8 files changed +319 -65
docker/run/fs/ins/post_install.sh
+1 -1
@@ -2,4 +2,4 @@
2
3 # Cleanup package list
4 rm -rf /var/lib/apt/lists/*
5 -# apt-get clean
\ No newline at end of file
5 +apt-get clean
\ No newline at end of file
python/api/mcp_server_get_detail.py new
+20
@@ -0,0 +1,20 @@
1 +from math import log
2 +from python.helpers.api import ApiHandler
3 +from flask import Request, Response
4 +
5 +from typing import Any
6 +
7 +from python.helpers.mcp_handler import MCPConfig
8 +
9 +
10 +class McpServerGetDetail(ApiHandler):
11 + async def process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response:
12 +
13 + # try:
14 + server_name = input.get("server_name")
15 + if not server_name:
16 + return {"success": False, "error": "Missing server_name"}
17 + detail = MCPConfig.get_instance().get_server_detail(server_name)
18 + return {"success": True, "detail": detail}
19 + # except Exception as e:
20 + # return {"success": False, "error": str(e)}
python/helpers/mcp_handler.py
+29 -8
@@ -5,6 +5,7 @@ from typing import (
5 Dict,
6 Optional,
7 Any,
8 + TextIO,
9 Union,
10 Literal,
11 Annotated,
@@ -623,6 +624,8 @@ class MCPConfig(BaseModel):
624 connected = True # tool_count > 0
625 # get error message if any
626 error = server.get_error()
627 + # get log bool
628 + has_log = server.get_log() != ""
629
630 # add server status to result
631 result.append(
@@ -631,6 +634,7 @@ class MCPConfig(BaseModel):
634 "connected": connected,
635 "error": error,
636 "tool_count": tool_count,
637 + "has_log": has_log,
638 }
639 )
640
@@ -642,11 +646,27 @@ class MCPConfig(BaseModel):
646 "connected": False,
647 "error": disconnected["error"],
648 "tool_count": 0,
649 + "has_log": False,
650 }
651 )
652
653 return result
654
655 + def get_server_detail(self, server_name: str) -> dict[str, Any]:
656 + with self.__lock:
657 + for server in self.servers:
658 + if server.name == server_name:
659 + try:
660 + tools = server.get_tools()
661 + except Exception as e:
662 + tools = []
663 + return {
664 + "name": server.name,
665 + "description": server.description,
666 + "tools": tools,
667 + }
668 + return {}
669 +
670 def is_initialized(self) -> bool:
671 """Check if the client is initialized"""
672 with self.__lock:
@@ -677,13 +697,16 @@ class MCPConfig(BaseModel):
697 for server in self.servers:
698 if server.name in server_names:
699 server_name = server.name
700 + prompt += f"### {server_name}\n"
701 + prompt += f"{server.description}\n\n"
702 +
703 for tool in server.get_tools():
704 prompt += (
705 f"### {server_name}.{tool['name']}:\n"
706 f"{tool['description']}\n\n"
684 - f"#### Categories:\n"
685 - f"* kind: MCP Server Tool\n"
686 - f'* server: "{server_name}" ({server.description})\n\n'
707 + # f"#### Categories:\n"
708 + # f"* kind: MCP Server Tool\n"
709 + # f'* server: "{server_name}" ({server.description})\n\n'
710 f"#### Arguments:\n"
711 )
712
@@ -783,6 +806,7 @@ class MCPClientBase(ABC):
806 self.tools: List[dict[str, Any]] = [] # Tools are cached on the client instance
807 self.error: str = ""
808 self.log: List[str] = []
809 + self.log_file: Optional[TextIO] = None
810
811 # Protected method
812 @abstractmethod
@@ -946,8 +970,6 @@ class MCPClientBase(ABC):
970 f"MCPClientBase::Failed to call tool '{tool_name}' on server '{self.server.name}'. Original error: {type(e).__name__}: {e}"
971 )
972
949 -
950 -class MCPClientLocal(MCPClientBase):
973 def get_log(self):
974 # read and return lines from self.log_file, do not close it
975 if not hasattr(self, 'log_file') or self.log_file is None:
@@ -959,6 +981,8 @@ class MCPClientLocal(MCPClientBase):
981 log = ""
982 return log
983
984 +
985 +class MCPClientLocal(MCPClientBase):
986 def __del__(self):
987 # close the log file if it exists
988 if hasattr(self, 'log_file') and self.log_file is not None:
@@ -1006,9 +1030,6 @@ class MCPClientLocal(MCPClientBase):
1030
1031 class MCPClientRemote(MCPClientBase):
1032
1009 - def get_log(self):
1010 - return "Logging not implemented for remote servers yet"
1011 -
1033 async def _create_stdio_transport(
1034 self, current_exit_stack: AsyncExitStack
1035 ) -> tuple[
run_ui.py
+11 -2
@@ -186,8 +186,9 @@ def run():
186 PrintStyle().error(f"Failed to start Cloudflare tunnel: {e}")
187 PrintStyle().print("Continuing without tunnel...")
188
189 - # initialize contexts from persisted chats
190 - persist_chat.load_tmp_chats()
189 + # # initialize contexts from persisted chats - moved to async task
190 + # persist_chat.load_tmp_chats()
191 +
192 # # reload scheduler
193 # scheduler = TaskScheduler.get()
194 # asyncio.run(scheduler.reload())
@@ -301,12 +302,20 @@ def run():
302
303 process.set_server(server)
304 server.log_startup()
305 +
306 + # Start init_a0 in a background thread when server starts
307 + import threading
308 + threading.Thread(target=init_a0, daemon=True).start()
309 +
310 server.serve_forever()
311 finally:
312 # Clean up tunnel if it was started
313 if tunnel:
314 tunnel.stop()
315
316 +def init_a0():
317 + # initialize contexts from persisted chats
318 + persist_chat.load_tmp_chats()
319
320 # run the internal server
321 if __name__ == "__main__":
webui/components/settings/mcp/client/mcp-server-descripton.html new
+24
@@ -0,0 +1,24 @@
1 +<html>
2 +
3 +<head>
4 + <title>MCP Server Detail</title>
5 +
6 + <script type="module">
7 + import { store } from "/components/settings/mcp/client/mcp-servers-store.js";
8 + </script>
9 +</head>
10 +
11 +<body>
12 + <div x-data>
13 + <template x-if="$store.mcpServersStore">
14 + <h3 x-text="$store.mcpServersStore.serverDetail.name"></h3>
15 + <p x-text="$store.mcpServersStore.serverDetail.description"></p>
16 + </template>
17 + </div>
18 +
19 + <style>
20 + </style>
21 +
22 +</body>
23 +
24 +</html>
\ No newline at end of file
webui/components/settings/mcp/client/mcp-server-tools.html new
+113
@@ -0,0 +1,113 @@
1 +<html>
2 +
3 +<head>
4 + <title>MCP Server Detail</title>
5 +
6 + <script type="module">
7 + import { store } from "/components/settings/mcp/client/mcp-servers-store.js";
8 + </script>
9 +</head>
10 +
11 +<body>
12 + <div x-data>
13 + <template x-if="$store.mcpServersStore">
14 + <div>
15 + <h3 x-text="$store.mcpServersStore.serverDetail.name"></h3>
16 + <p x-text="$store.mcpServersStore.serverDetail.description"></p>
17 +
18 + <div class="tools-container">
19 + <template x-for="tool in $store.mcpServersStore.serverDetail.tools" :key="tool.name">
20 + <div class="tool-item">
21 + <h4 x-text="tool.name"></h4>
22 + <p class="tool-description" x-text="tool.description"></p>
23 +
24 + <template x-if="tool.input_schema?.properties">
25 + <div class="tool-properties">
26 + <p class="properties-title">Properties:</p>
27 + <ul>
28 + <template x-for="(prop, propName) in tool.input_schema.properties"
29 + :key="propName">
30 + <li>
31 + <span class="prop-name" x-text="propName"></span>:
32 + <span class="prop-type" x-text="prop.type || 'any'"></span>
33 + <template x-if="prop.description">
34 + <span class="prop-desc" x-text="' - ' + prop.description"></span>
35 + </template>
36 + </li>
37 + </template>
38 + </ul>
39 + </div>
40 + </template>
41 + </div>
42 + </template>
43 + </div>
44 + </div>
45 + </template>
46 + </div>
47 +
48 + <style>
49 + .tools-container {
50 + margin-top: 1.5em;
51 + display: flex;
52 + flex-direction: column;
53 + gap: 1.2em;
54 + }
55 +
56 + .tool-item {
57 + padding: 1em;
58 + border: 1px solid rgba(192, 192, 192, 0.16);
59 + border-radius: 4px;
60 + }
61 +
62 + .tool-item h4 {
63 + margin-top: 0;
64 + margin-bottom: 0.5em;
65 + font-size: 1.1em;
66 + }
67 +
68 + .tool-description {
69 + margin-bottom: 1em;
70 + color: var(--c-fg);
71 + line-height: 1.4;
72 + }
73 +
74 + .tool-properties {
75 + margin-top: 0.8em;
76 + padding: 0.8em;
77 + background-color: rgba(0, 0, 0, 0.04);
78 + border-radius: 3px;
79 + }
80 +
81 + .properties-title {
82 + font-weight: 600;
83 + margin-top: 0;
84 + margin-bottom: 0.5em;
85 + }
86 +
87 + .tool-properties ul {
88 + margin: 0;
89 + padding-left: 1.5em;
90 + }
91 +
92 + .tool-properties li {
93 + margin-bottom: 0.3em;
94 + }
95 +
96 + .prop-name {
97 + font-weight: 600;
98 + color: var(--c-accent);
99 + }
100 +
101 + .prop-type {
102 + color: var(--c-fg2);
103 + font-style: italic;
104 + }
105 +
106 + .prop-desc {
107 + color: var(--c-fg);
108 + }
109 + </style>
110 +
111 +</body>
112 +
113 +</html>
\ No newline at end of file
webui/components/settings/mcp/client/mcp-servers-store.js
+21 -1
@@ -115,7 +115,27 @@ const model = {
115 });
116 if (resp.success) {
117 this.serverLog = resp.log;
118 - openModal("settings/mcp/client/mcp-servers-log.html")
118 + openModal("settings/mcp/client/mcp-servers-log.html");
119 + }
120 + },
121 +
122 + async onServerClick(serverName) {
123 + const resp = await API.callJsonApi("mcp_server_get_detail", {
124 + server_name: serverName,
125 + });
126 + if (resp.success) {
127 + this.serverDetail = resp.detail;
128 + openModal("settings/mcp/client/mcp-server-description.html");
129 + }
130 + },
131 +
132 + async onToolCountClick(serverName) {
133 + const resp = await API.callJsonApi("mcp_server_get_detail", {
134 + server_name: serverName,
135 + });
136 + if (resp.success) {
137 + this.serverDetail = resp.detail;
138 + openModal("settings/mcp/client/mcp-server-tools.html");
139 }
140 },
141 };
webui/components/settings/mcp/client/mcp-servers.html
+100 -53
@@ -27,24 +27,41 @@
27 <div class="server-list">
28 <template x-for="server in $store.mcpServersStore.servers" :key="server.name">
29 <div class="server-item">
30 - <div class="status-icon" style="margin-right: 0.5em;"
31 - x-data="{ connected: server.connected }">
32 - <svg viewBox="0 0 30 30">
33 - <!-- Connected State (filled circle, green or orange) -->
34 - <circle x-bind:class="server.error ? 'disconnected-circle' : 'connected-circle'" cx="15" cy="15" r="8" x-bind:fill="server.connected
35 - ? (server.error ? '#e40138' : (server.tool_count === 0 ? '#e40138' : '#00c340'))
36 - : 'none'" x-bind:opacity="server.connected ? 1 : 0" />
37 -
38 - <!-- Disconnected State (outline circle) -->
39 - <circle x-bind:class="server.error ? 'disconnected-circle' : 'connected-circle'" cx="15" cy="15" r="9" fill="none"
40 - stroke="#e40138" stroke-width="3" x-bind:opacity="server.connected ? 0 : 1" />
41 - </svg>
30 + <div class="server-main-row">
31 + <!-- Status indicator -->
32 + <div class="status-dot" x-data="{ connected: server.connected }">
33 + <svg viewBox="0 0 16 16" width="12" height="12">
34 + <circle cx="8" cy="8" r="6"
35 + x-bind:fill="server.connected
36 + ? (server.error ? '#e40138' : (server.tool_count > 0 ? '#00c340' : '#e40138'))
37 + : 'none'"
38 + x-bind:opacity="server.connected ? 1 : 0" />
39 + <circle cx="8" cy="8" r="6" fill="none"
40 + stroke="#e40138" stroke-width="2"
41 + x-bind:opacity="server.connected ? 0 : 1" />
42 + </svg>
43 + </div>
44 +
45 + <!-- Server name -->
46 + <span class="server-name" x-text="server.name"></span>
47 +
48 + <!-- Tool count (clickable if > 0, only for connected servers without errors) -->
49 + <span class="tool-count"
50 + x-show="server.tool_count > 0"
51 + @click="$store.mcpServersStore.onToolCountClick && $store.mcpServersStore.onToolCountClick(server.name)"
52 + x-text="server.tool_count + ' tools'"></span>
53 +
54 + <!-- Disabled in config message -->
55 + <span class="config-status" x-show="server.disabled_in_config">Disabled in config</span>
56 +
57 + <!-- Log button (only shown if has_log is true) -->
58 + <span class="log-btn" x-show="server.has_log" @click="$store.mcpServersStore.getServerLog(server.name)">Log</span>
59 + </div>
60 +
61 + <!-- Error message (if any) -->
62 + <div class="server-error-row" x-show="server.error">
63 + <span class="server-error" x-text="server.error"></span>
64 </div>
43 - <span class="server-name" x-text="server.name"></span>
44 - <span class="server-tools"
45 - x-text="'- ' + (server.tool_count ? server.tool_count : 0) + ' tools'"></span>
46 - <span class="server-error" x-show="server.error" x-text="server.error"></span>
47 - <span class="server-links"><a href="#" @click="$store.mcpServersStore.getServerLog(server.name)">View Log</a></span>
65 </div>
66 </template>
67 <div x-show="$store.mcpServersStore.servers.length === 0" class="no-servers">
@@ -70,56 +87,86 @@
87
88 .server-item {
89 display: flex;
73 - align-items: center;
74 - flex-wrap: wrap;
75 - padding: 0.5em;
76 - border-bottom: 1px solid var(--c-border);
90 + flex-direction: column;
91 + padding: 0.5em 0.7em;
92 + margin-bottom: 0.4em;
93 + min-height: 2.2em; /* Ensure consistent height even without errors */
94 + border: 1px solid rgba(192, 192, 192, 0.161); /* Silver with 30% opacity */
95 + border-radius: 4px;
96 }
97
79 - .server-item:last-child {
80 - border-bottom: none;
81 - }
82 -
83 - .status-indicator {
84 - display: inline-block;
85 - width: 0.8em;
86 - height: 0.8em;
87 - border-radius: 50%;
88 - margin-right: 0.8em;
98 + .server-list {
99 + margin-top: 0.5em;
100 + margin-bottom: 1em;
101 + display: flex;
102 + flex-direction: column;
103 + gap: 0.2em;
104 }
90 -
91 - .status-indicator.connected {
92 - background-color: #4CAF50;
105 +
106 + .server-main-row {
107 + display: flex;
108 + align-items: center;
109 + gap: 0.8em;
110 + width: 100%;
111 }
112
95 - .status-indicator.disconnected {
96 - background-color: #F44336;
113 + .status-dot {
114 + display: flex;
115 + align-items: center;
116 + justify-content: center;
117 }
98 -
118 +
119 .server-name {
100 - font-weight: 500;
101 - margin-right: 0.5em;
120 + font-weight: 600;
121 + min-width: 12em;
122 }
103 -
104 - .server-tools {
123 +
124 + .tool-count {
125 color: var(--c-fg2);
126 font-size: 0.9em;
127 + user-select: none;
128 }
108 -
109 - .no-servers {
110 - padding: 1em;
111 - color: var(--c-fg2);
112 - font-style: italic;
129 +
130 + .tool-count {
131 + cursor: default;
132 }
114 -
115 - .server-error {
116 - color: #F44336;
133 +
134 + .tool-count:hover {
135 + opacity: 0.8;
136 + cursor: pointer;
137 + }
138 +
139 + .config-status {
140 + color: #e40138;
141 + font-size: 0.85em;
142 + opacity: 0.8;
143 + }
144 +
145 + .log-btn {
146 + margin-left: auto;
147 font-size: 0.9em;
118 - margin-left: 0.8em;
148 + cursor: pointer;
149 + text-decoration: none;
150 + opacity: 0.85;
151 }
120 -
121 - .server-links {
122 - margin-left: 0.8em;
152 +
153 + .log-btn:hover {
154 + opacity: 1;
155 + }
156 +
157 + .server-error-row {
158 + margin-left: 1.8em;
159 + margin-top: 0.1em;
160 + font-size: 0.8em;
161 + color: #F44336;
162 + opacity: 0.85;
163 + line-height: 1.2;
164 + }
165 +
166 + .no-servers {
167 + padding: 0.5em;
168 + color: var(--c-fg2);
169 + font-style: italic;
170 }
171 </style>
172