Skip to content

Commit d5c494e

Browse files
committed
Refactor: Address UI feedback for Tools and Servers pages layout and comments
1 parent a3e868e commit d5c494e

File tree

7 files changed

+247
-82
lines changed

7 files changed

+247
-82
lines changed

config.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: "MCP Proxy Server"
2-
version: "0.1.8"
2+
version: "0.1.9"
33
slug: "mcp_proxy_server"
44
description: "A central hub for Model Context Protocol (MCP) servers. Manages multiple backend MCP servers (Stdio/SSE), exposing their combined tools and resources via a unified SSE interface or as a Stdio server. Features Web UI for server/tool management, real-time installation monitoring, and optional web terminal."
55
arch:
@@ -11,9 +11,11 @@ hassio_role: default
1111
homeassistant_api: false
1212
host_network: false
1313
map:
14-
- "config:/mcp-proxy-server/config:rw" # App config (mcp_server.json etc.) in /mcp-proxy-server/config inside container, maps to HA's config dir for this addon
15-
- "share:rw" # For TOOLS_FOLDER, maps to /share inside container
16-
# - "ssl:ro"
14+
- type: addon_config
15+
read_only: False
16+
path: /mcp-proxy-server/config # App config (mcp_server.json etc.) in /mcp-proxy-server/config inside container, maps to HA's config dir for this addon
17+
- type: share
18+
read_only: False # For TOOLS_FOLDER, maps to /share inside container
1719
ports:
1820
"3663/tcp": 3663
1921
ports_description:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mcp-proxy-server",
3-
"version": "0.1.8",
3+
"version": "0.1.9",
44
"author": "ptbsare",
55
"license": "MIT",
66
"description": "An MCP proxy server that aggregates and serves multiple MCP resource servers through a single interface with stdio/sse support",

public/servers.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ function renderServerEntry(key, serverConf, startExpanded = false) {
5656

5757
const headerDiv = document.createElement('div');
5858
headerDiv.classList.add('server-header');
59+
// Move Active checkbox to the header, at the beginning
5960
headerDiv.innerHTML = `
61+
<label class="inline-label server-active-label" title="Activate/Deactivate Server">
62+
<input type="checkbox" class="server-active-input" ${serverConf.active !== false ? 'checked' : ''}>
63+
</label>
6064
<h3>${serverConf.name || key} (<span class="server-type">${type}</span>)</h3>
6165
<button class="delete-button">Delete</button>
6266
`;
@@ -65,15 +69,10 @@ function renderServerEntry(key, serverConf, startExpanded = false) {
6569
const detailsDiv = document.createElement('div');
6670
detailsDiv.classList.add('server-details');
6771

72+
// Remove Active checkbox from detailsHtml
6873
let detailsHtml = `
6974
<div><label>Server Key (Unique ID):</label><input type="text" class="server-key-input" value="${key}" required></div>
7075
<div><label>Display Name:</label><input type="text" class="server-name-input" value="${serverConf.name || ''}"></div>
71-
<div>
72-
<label class="inline-label">
73-
<input type="checkbox" class="server-active-input" ${serverConf.active !== false ? 'checked' : ''}>
74-
Active
75-
</label>
76-
</div>
7776
`;
7877

7978
if (isSSE) {

public/style.css

Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -214,40 +214,103 @@ button:disabled, .add-button:disabled {
214214
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
215215
}
216216

217-
.server-entry .server-header {
217+
/* Shared header styles for server and tool entries */
218+
.server-entry .server-header, .tool-entry .tool-header {
218219
display: flex;
219-
justify-content: space-between;
220-
align-items: center;
221-
margin-bottom: 1rem;
220+
align-items: center; /* Key for vertical alignment of items in the row */
221+
margin-bottom: 1rem;
222+
/* justify-content: flex-start; /* Let items flow and use margins/flex-grow for spacing */
222223
}
223224

224-
.server-entry .server-header h3 {
225-
margin: 0;
226-
color: #007bff;
225+
.server-entry .server-header h3, .tool-entry .tool-header h3 {
226+
margin: 0;
227+
color: #007bff;
227228
cursor: pointer;
228-
flex-grow: 1;
229-
padding-right: 1rem;
229+
flex-grow: 1; /* Allow h3 to take available space */
230+
padding-right: 1rem; /* Space after h3, before next inline element */
230231
}
231-
.server-entry .server-header h3:hover {
232+
.server-entry .server-header h3:hover, .tool-entry .tool-header h3:hover {
232233
text-decoration: underline;
233234
}
234235

235-
.server-entry .server-details {
236-
padding-left: 1rem;
236+
/* Style for the "Active" checkbox label in server header */
237+
.server-entry .server-header .server-active-label {
238+
margin-right: 0.75rem; /* Space after checkbox, before H3 */
239+
display: inline-flex;
240+
align-items: center; /* Align items within the label itself */
241+
}
242+
.server-entry .server-header .server-active-label input[type="checkbox"] {
243+
margin: 0;
244+
vertical-align: middle; /* Helps align checkbox with potential (now removed) text in label */
245+
}
246+
247+
248+
/* Specific to tool header for additional elements */
249+
.tool-entry .tool-header {
250+
/* display: flex; align-items: center; are shared */
251+
flex-wrap: wrap; /* Allow items to wrap to the next line if space is insufficient */
252+
}
253+
254+
.tool-entry .tool-header .tool-exposed-name {
255+
font-size: 0.85em;
256+
color: #6c757d;
257+
margin-left: 0.5rem; /* Space after H3 */
258+
white-space: normal; /* Allow long text to wrap */
259+
/* margin-right: auto; /* Remove this, as there's no element to push to the far right anymore */
260+
/* Adding a small flex-shrink to allow it to shrink if needed, but h3 should grow more */
261+
flex-shrink: 1;
262+
}
263+
264+
/* Style for the "Enabled" checkbox label in tool header (now at the start) */
265+
.tool-entry .tool-header .tool-enable-label {
266+
margin-right: 0.75rem; /* Space after checkbox, before H3 */
267+
display: inline-flex;
268+
align-items: center;
269+
}
270+
.tool-entry .tool-header .tool-enable-label input[type="checkbox"] {
271+
margin: 0;
272+
vertical-align: middle; /* Consistent vertical alignment */
273+
}
274+
275+
/* Styles for server header (mostly covered by shared now) */
276+
.server-entry .server-header {
277+
/* display: flex; align-items: center; are shared */
278+
/* justify-content: flex-start; /* Let items flow */
279+
}
280+
281+
/* Delete button specific styling if needed, assuming it's the last element */
282+
.server-entry .server-header .delete-button {
283+
margin-left: auto; /* Push delete button to the far right */
284+
flex-shrink: 0; /* Prevent delete button from shrinking */
285+
}
286+
287+
288+
/* Shared details styles for server and tool entries */
289+
.server-entry .server-details, .tool-entry .tool-details {
290+
padding-left: 1rem;
237291
border-left: 2px solid #e9ecef;
238-
transition: max-height 0.3s ease-out, opacity 0.3s ease-out;
239-
overflow: hidden;
240-
max-height: 2000px;
292+
transition: max-height 0.3s ease-out, opacity 0.3s ease-out, margin-top 0.3s ease-out, padding-top 0.3s ease-out, padding-bottom 0.3s ease-out;
293+
overflow: hidden;
294+
max-height: 2000px; /* Arbitrary large number for expanded state */
241295
opacity: 1;
296+
margin-top: 1rem; /* Add some space when expanded */
297+
padding-top: 1rem; /* Add padding when expanded */
298+
padding-bottom: 1rem; /* Add padding when expanded */
242299
}
243300

244-
.server-entry.collapsed .server-details {
245-
max-height: 0;
301+
.server-entry.collapsed .server-details, .tool-entry.collapsed .tool-details {
302+
max-height: 0;
246303
opacity: 0;
247-
padding-left: 1rem;
248-
border-left: 2px solid #e9ecef;
249-
margin-top: 0;
250-
overflow: hidden;
304+
margin-top: 0;
305+
padding-top: 0;
306+
padding-bottom: 0;
307+
border-left-width: 0; /* Hide border when collapsed for compactness */
308+
padding-left: 0; /* Remove padding when collapsed */
309+
}
310+
311+
/* Reduce bottom margin of header when entry is collapsed for compactness */
312+
.server-entry.collapsed .server-header, .tool-entry.collapsed .tool-header {
313+
margin-bottom: 0;
251314
}
252315

253316
.server-entry label, .tool-entry label {

public/tools.js

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ function renderTools() {
6666

6767
// Render discovered tools first, merging with config
6868
discoveredTools.forEach(tool => {
69-
const toolKey = `${tool.server_name}--${tool.name}`; // Unique key
69+
const toolKey = `${tool.serverName}--${tool.name}`; // Unique key
7070
const config = currentToolConfig.tools[toolKey] || {}; // Get config or empty object
7171
renderToolEntry(toolKey, tool, config);
7272
configuredToolKeys.delete(toolKey); // Remove from set as it's handled
@@ -89,61 +89,133 @@ function renderToolEntry(toolKey, toolDefinition, toolConfig, isConfigOnly = fal
8989
if (!toolListDiv) return; // Guard
9090
const entryDiv = document.createElement('div');
9191
entryDiv.classList.add('tool-entry');
92-
entryDiv.dataset.toolKey = toolKey;
92+
entryDiv.classList.add('collapsed'); // Add collapsed class by default
93+
entryDiv.dataset.toolKey = toolKey; // Store the original key
94+
95+
// Determine the name and description exposed to the model
96+
const exposedName = toolConfig.exposedName || toolKey;
97+
const exposedDescription = toolConfig.exposedDescription || toolDefinition?.description || ''; // Use override, fallback to original, then empty string
98+
99+
// Get potential overrides from config for UI input fields
100+
const exposedNameOverride = toolConfig.exposedName || '';
101+
const exposedDescriptionOverride = toolConfig.exposedDescription || '';
93102

94-
const displayName = toolConfig.displayName || (toolDefinition ? `${toolDefinition.server_name} / ${toolDefinition.name}` : toolKey);
95-
const description = toolConfig.description || toolDefinition?.description || (isConfigOnly ? 'Description not available (tool not discovered)' : 'No description provided.');
96103
const isEnabled = toolConfig.enabled !== false; // Enabled by default
104+
const originalDescription = toolDefinition?.description || 'N/A'; // Original description for display
97105

98106
entryDiv.innerHTML = `
99107
<div class="tool-header">
100-
<h3>${displayName}</h3>
101-
<span class="tool-key">(${toolKey})</span>
102-
<label class="inline-label tool-enable-toggle">
108+
<label class="inline-label tool-enable-label" title="Enable/Disable Tool">
103109
<input type="checkbox" class="tool-enabled-input" ${isEnabled ? 'checked' : ''}>
104-
Enabled
105110
</label>
111+
<h3>${toolKey}</h3>
112+
<span class="tool-exposed-name">Exposed As: ${exposedName}</span>
106113
</div>
107114
<div class="tool-details">
108-
<div><label>Display Name Override:</label><input type="text" class="tool-displayname-input" value="${toolConfig.displayName || ''}" placeholder="Optional: Override default name"></div>
109-
<div><label>Description Override:</label><textarea class="tool-description-input" placeholder="Optional: Override default description">${toolConfig.description || ''}</textarea></div>
110-
<p class="tool-original-description">Original Description: ${toolDefinition?.description || 'N/A'}</p>
115+
<div>
116+
<label>Exposed Tool Name Override (Optional):</label>
117+
<input type="text" class="tool-exposedname-input" value="${exposedNameOverride}" placeholder="Default: ${toolKey}">
118+
<small>Overrides the name exposed to AI models. Must be unique and contain only letters, numbers, _, - (not starting with a number).</small>
119+
</div>
120+
<div>
121+
<label>Exposed Description Override (Optional):</label>
122+
<textarea class="tool-exposeddescription-input" placeholder="Default: (Original Description below)">${exposedDescriptionOverride}</textarea>
123+
</div>
124+
<p class="tool-original-description">Original Description: ${originalDescription}</p>
111125
${isConfigOnly ? '<p class="warning-message">This tool was configured but not discovered by any active server.</p>' : ''}
112126
</div>
113127
`;
114128

115-
toolListDiv.appendChild(entryDiv);
129+
toolListDiv.appendChild(entryDiv); // Append first, then query elements within it
130+
131+
// Add click listener to the header (h3) to toggle collapse
132+
const headerH3 = entryDiv.querySelector('.tool-header h3');
133+
if (headerH3) {
134+
headerH3.style.cursor = 'pointer'; // Indicate it's clickable
135+
headerH3.addEventListener('click', () => {
136+
entryDiv.classList.toggle('collapsed');
137+
});
138+
}
116139
}
117140

118141
function initializeToolSaveListener() {
119142
if (!saveToolConfigButton || !toolListDiv || !saveToolStatus) return; // Guard
120143

144+
// Regex for validating exposed tool name override
145+
const validToolNameRegex = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
146+
121147
saveToolConfigButton.addEventListener('click', async () => {
122-
saveToolStatus.textContent = 'Saving tool configuration...';
148+
saveToolStatus.textContent = 'Validating and saving tool configuration...';
123149
saveToolStatus.style.color = 'orange';
124150
const newToolConfig = { tools: {} };
125151
const entries = toolListDiv.querySelectorAll('.tool-entry');
152+
let isValid = true;
153+
let errorMsg = '';
154+
const exposedNames = new Set(); // To check for duplicates
126155

127156
entries.forEach(entryDiv => {
128-
const toolKey = entryDiv.dataset.toolKey;
157+
if (!isValid) return; // Stop processing if an error occurred
158+
159+
const toolKey = entryDiv.dataset.toolKey; // Original key
129160
const enabledInput = entryDiv.querySelector('.tool-enabled-input');
130-
const displayNameInput = entryDiv.querySelector('.tool-displayname-input');
131-
const descriptionInput = entryDiv.querySelector('.tool-description-input');
161+
const exposedNameInput = entryDiv.querySelector('.tool-exposedname-input');
162+
const exposedDescriptionInput = entryDiv.querySelector('.tool-exposeddescription-input');
163+
164+
const exposedNameOverride = exposedNameInput.value.trim();
165+
const exposedDescriptionOverride = exposedDescriptionInput.value.trim();
166+
const isEnabled = enabledInput.checked;
167+
168+
const finalExposedName = exposedNameOverride || toolKey; // Use override or fallback to original key
169+
170+
// --- Validation ---
171+
// 1. Validate format of the override (if provided)
172+
if (exposedNameOverride && !validToolNameRegex.test(exposedNameOverride)) {
173+
isValid = false;
174+
errorMsg = `Invalid format for Exposed Tool Name Override "${exposedNameOverride}" for tool "${toolKey}". Use letters, numbers, _, - (cannot start with number).`;
175+
exposedNameInput.style.border = '1px solid red';
176+
return;
177+
} else {
178+
exposedNameInput.style.border = ''; // Reset border on valid or empty
179+
}
180+
181+
// 2. Check for duplicate exposed names (considering overrides)
182+
if (exposedNames.has(finalExposedName)) {
183+
isValid = false;
184+
errorMsg = `Duplicate Exposed Tool Name: "${finalExposedName}". Please ensure all exposed names (including overrides) are unique.`;
185+
// Highlight the input that caused the duplicate
186+
exposedNameInput.style.border = '1px solid red';
187+
// Optionally, find and highlight the previous entry with the same name
188+
return;
189+
}
190+
exposedNames.add(finalExposedName);
191+
// --- End Validation ---
192+
132193

133194
const configData = {
134-
enabled: enabledInput.checked,
135-
displayName: displayNameInput.value.trim() || undefined, // Store undefined if empty
136-
description: descriptionInput.value.trim() || undefined, // Store undefined if empty
195+
enabled: isEnabled,
196+
// Only store overrides if they are actually set
197+
exposedName: exposedNameOverride || undefined,
198+
exposedDescription: exposedDescriptionOverride || undefined,
137199
};
138200

139201
// Only store config if it differs from default (enabled=true, no overrides)
140-
// Or if it's explicitly disabled
141-
if (configData.enabled === false || configData.displayName || configData.description) {
202+
// Or if it's explicitly disabled, or if overrides are set
203+
if (configData.enabled === false || configData.exposedName || configData.exposedDescription) {
142204
newToolConfig.tools[toolKey] = configData;
143205
}
144206
});
145207

208+
// If validation failed, show error and stop
209+
if (!isValid) {
210+
saveToolStatus.textContent = `Error: ${errorMsg}`;
211+
saveToolStatus.style.color = 'red';
212+
setTimeout(() => { if(saveToolStatus) saveToolStatus.textContent = ''; saveToolStatus.style.color = 'green'; }, 7000);
213+
return;
214+
}
215+
216+
// Proceed to save if valid
146217
try {
218+
saveToolStatus.textContent = 'Saving tool configuration...'; // Update status after validation
147219
const response = await fetch('/admin/tools/config', {
148220
method: 'POST',
149221
headers: { 'Content-Type': 'application/json' },

src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export interface Config {
2828

2929
export interface ToolSettings {
3030
enabled: boolean;
31+
exposedName?: string;
32+
exposedDescription?: string;
3133
}
3234

3335
export interface ToolConfig {

0 commit comments

Comments
 (0)