aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src
diff options
context:
space:
mode:
Diffstat (limited to 'renderer/src')
-rw-r--r--renderer/src/components/settings/McpSettings.tsx200
-rw-r--r--renderer/src/styles/globals.css74
2 files changed, 237 insertions, 37 deletions
diff --git a/renderer/src/components/settings/McpSettings.tsx b/renderer/src/components/settings/McpSettings.tsx
index 7d23fe4..3c4f8e5 100644
--- a/renderer/src/components/settings/McpSettings.tsx
+++ b/renderer/src/components/settings/McpSettings.tsx
@@ -4,6 +4,11 @@ const api = window.api;
4 4
5type McpServerType = "stdio" | "sse" | "http"; 5type McpServerType = "stdio" | "sse" | "http";
6 6
7interface McpTool {
8 name: string;
9 description?: string;
10}
11
7interface McpServerConfig { 12interface McpServerConfig {
8 type: McpServerType; 13 type: McpServerType;
9 command?: string; 14 command?: string;
@@ -11,6 +16,9 @@ interface McpServerConfig {
11 env?: Record<string, string>; 16 env?: Record<string, string>;
12 url?: string; 17 url?: string;
13 headers?: Record<string, string>; 18 headers?: Record<string, string>;
19 // Tool management
20 discoveredTools?: McpTool[];
21 enabledTools?: string[]; // If undefined/empty after discovery, all tools enabled
14} 22}
15 23
16type McpServersConfig = Record<string, McpServerConfig>; 24type McpServersConfig = Record<string, McpServerConfig>;
@@ -33,6 +41,7 @@ export function McpSettings() {
33 const [editing, setEditing] = useState<EditingServer | null>(null); 41 const [editing, setEditing] = useState<EditingServer | null>(null);
34 const [saveStatus, setSaveStatus] = useState<"idle" | "saved" | "error">("idle"); 42 const [saveStatus, setSaveStatus] = useState<"idle" | "saved" | "error">("idle");
35 const [error, setError] = useState<string | null>(null); 43 const [error, setError] = useState<string | null>(null);
44 const [discovering, setDiscovering] = useState<string | null>(null); // server name being discovered
36 45
37 useEffect(() => { 46 useEffect(() => {
38 api.getSettings(["mcpServers"]).then((settings) => { 47 api.getSettings(["mcpServers"]).then((settings) => {
@@ -70,6 +79,62 @@ export function McpSettings() {
70 await saveServers(rest); 79 await saveServers(rest);
71 }; 80 };
72 81
82 const handleDiscoverTools = async (name: string) => {
83 if (!servers) return;
84 const config = servers[name];
85
86 setDiscovering(name);
87 setError(null);
88
89 try {
90 const result = await api.discoverMcpTools({
91 type: config.type,
92 command: config.command,
93 args: config.args,
94 env: config.env,
95 url: config.url,
96 });
97
98 if (result.error) {
99 setError(`Discovery failed: ${result.error}`);
100 } else {
101 // Save discovered tools, enable all by default
102 const newServers = {
103 ...servers,
104 [name]: {
105 ...config,
106 discoveredTools: result.tools,
107 enabledTools: result.tools.map((t) => t.name),
108 },
109 };
110 await saveServers(newServers);
111 }
112 } catch (e) {
113 setError(`Discovery failed: ${String(e)}`);
114 } finally {
115 setDiscovering(null);
116 }
117 };
118
119 const handleToggleTool = async (serverName: string, toolName: string, enabled: boolean) => {
120 if (!servers) return;
121 const config = servers[serverName];
122 const currentEnabled = config.enabledTools || config.discoveredTools?.map((t) => t.name) || [];
123
124 const newEnabled = enabled
125 ? [...currentEnabled, toolName]
126 : currentEnabled.filter((t) => t !== toolName);
127
128 const newServers = {
129 ...servers,
130 [serverName]: {
131 ...config,
132 enabledTools: newEnabled,
133 },
134 };
135 await saveServers(newServers);
136 };
137
73 const handleSaveEdit = async () => { 138 const handleSaveEdit = async () => {
74 if (!editing || !servers) return; 139 if (!editing || !servers) return;
75 const { originalName, name, config } = editing; 140 const { originalName, name, config } = editing;
@@ -113,6 +178,21 @@ export function McpSettings() {
113 } 178 }
114 } 179 }
115 180
181 // Preserve discovered tools if config didn't change substantially
182 const oldConfig = originalName ? servers[originalName] : null;
183 if (oldConfig?.discoveredTools) {
184 const configChanged =
185 config.type !== oldConfig.type ||
186 config.command !== oldConfig.command ||
187 JSON.stringify(config.args) !== JSON.stringify(oldConfig.args) ||
188 config.url !== oldConfig.url;
189
190 if (!configChanged) {
191 cleanConfig.discoveredTools = oldConfig.discoveredTools;
192 cleanConfig.enabledTools = oldConfig.enabledTools;
193 }
194 }
195
116 newServers[name.trim()] = cleanConfig; 196 newServers[name.trim()] = cleanConfig;
117 await saveServers(newServers); 197 await saveServers(newServers);
118 setEditing(null); 198 setEditing(null);
@@ -139,13 +219,11 @@ export function McpSettings() {
139 <div className="settings-section-title">MCP Servers</div> 219 <div className="settings-section-title">MCP Servers</div>
140 <div className="settings-section-desc"> 220 <div className="settings-section-desc">
141 Configure Model Context Protocol servers to give Claude access to 221 Configure Model Context Protocol servers to give Claude access to
142 external data sources like Jira, documentation, or databases. Servers 222 external data sources. Click "Discover Tools" to fetch available tools
143 are available in all phases. 223 and select which ones to enable.
144 </div> 224 </div>
145 225
146 {error && ( 226 {error && <div className="mcp-error">{error}</div>}
147 <div className="mcp-error">{error}</div>
148 )}
149 227
150 {/* Server List */} 228 {/* Server List */}
151 {!editing && ( 229 {!editing && (
@@ -154,39 +232,87 @@ export function McpSettings() {
154 {serverNames.length === 0 ? ( 232 {serverNames.length === 0 ? (
155 <div className="mcp-empty">No MCP servers configured</div> 233 <div className="mcp-empty">No MCP servers configured</div>
156 ) : ( 234 ) : (
157 serverNames.map((name) => ( 235 serverNames.map((name) => {
158 <div key={name} className="mcp-server-card"> 236 const config = servers[name];
159 <div className="mcp-server-info"> 237 const hasTools = config.discoveredTools && config.discoveredTools.length > 0;
160 <span className="mcp-server-name">{name}</span> 238 const enabledTools = config.enabledTools || config.discoveredTools?.map((t) => t.name) || [];
161 <span className="mcp-server-type">{servers[name].type}</span> 239
162 <span className="mcp-server-detail"> 240 return (
163 {servers[name].type === "stdio" 241 <div key={name} className="mcp-server-card">
164 ? servers[name].command 242 <div className="mcp-server-header">
165 : servers[name].url} 243 <div className="mcp-server-info">
166 </span> 244 <span className="mcp-server-name">{name}</span>
245 <span className="mcp-server-type">{config.type}</span>
246 <span className="mcp-server-detail">
247 {config.type === "stdio" ? config.command : config.url}
248 </span>
249 </div>
250 <div className="mcp-server-actions">
251 <button
252 className="btn-secondary btn-small"
253 onClick={() => handleDiscoverTools(name)}
254 disabled={discovering === name}
255 >
256 {discovering === name ? "Discovering..." : "Discover Tools"}
257 </button>
258 <button
259 className="btn-secondary btn-small"
260 onClick={() =>
261 setEditing({
262 originalName: name,
263 name,
264 config: { ...config },
265 })
266 }
267 >
268 Edit
269 </button>
270 <button
271 className="btn-secondary btn-small btn-danger"
272 onClick={() => handleDelete(name)}
273 >
274 Delete
275 </button>
276 </div>
277 </div>
278
279 {/* Tools Section */}
280 {hasTools && (
281 <div className="mcp-tools-section">
282 <div className="mcp-tools-header">
283 Tools ({enabledTools.length}/{config.discoveredTools!.length} enabled)
284 </div>
285 <div className="mcp-tools-list">
286 {config.discoveredTools!.map((tool) => {
287 const isEnabled = enabledTools.includes(tool.name);
288 return (
289 <label key={tool.name} className="mcp-tool-item">
290 <input
291 type="checkbox"
292 checked={isEnabled}
293 onChange={(e) =>
294 handleToggleTool(name, tool.name, e.target.checked)
295 }
296 />
297 <span className="mcp-tool-name">{tool.name}</span>
298 {tool.description && (
299 <span className="mcp-tool-desc">{tool.description}</span>
300 )}
301 </label>
302 );
303 })}
304 </div>
305 </div>
306 )}
307
308 {!hasTools && (
309 <div className="mcp-tools-empty">
310 Click "Discover Tools" to see available tools
311 </div>
312 )}
167 </div> 313 </div>
168 <div className="mcp-server-actions"> 314 );
169 <button 315 })
170 className="btn-secondary btn-small"
171 onClick={() =>
172 setEditing({
173 originalName: name,
174 name,
175 config: { ...servers[name] },
176 })
177 }
178 >
179 Edit
180 </button>
181 <button
182 className="btn-secondary btn-small btn-danger"
183 onClick={() => handleDelete(name)}
184 >
185 Delete
186 </button>
187 </div>
188 </div>
189 ))
190 )} 316 )}
191 </div> 317 </div>
192 318
diff --git a/renderer/src/styles/globals.css b/renderer/src/styles/globals.css
index 6cf3dc7..20275ae 100644
--- a/renderer/src/styles/globals.css
+++ b/renderer/src/styles/globals.css
@@ -1348,3 +1348,77 @@ html[data-theme="light"] .settings-textarea:focus {
1348 padding-top: 16px; 1348 padding-top: 16px;
1349 border-top: 1px solid var(--border); 1349 border-top: 1px solid var(--border);
1350} 1350}
1351
1352/* MCP Server Card with Tools */
1353.mcp-server-card {
1354 display: flex;
1355 flex-direction: column;
1356 padding: 12px;
1357 background: var(--bg-secondary);
1358 border: 1px solid var(--border);
1359 border-radius: 4px;
1360}
1361
1362.mcp-server-header {
1363 display: flex;
1364 align-items: center;
1365 justify-content: space-between;
1366 gap: 12px;
1367}
1368
1369.mcp-tools-section {
1370 margin-top: 12px;
1371 padding-top: 12px;
1372 border-top: 1px solid var(--border);
1373}
1374
1375.mcp-tools-header {
1376 font-size: 11px;
1377 font-weight: 500;
1378 text-transform: uppercase;
1379 letter-spacing: 0.05em;
1380 color: var(--text-secondary);
1381 margin-bottom: 8px;
1382}
1383
1384.mcp-tools-list {
1385 display: flex;
1386 flex-direction: column;
1387 gap: 6px;
1388}
1389
1390.mcp-tool-item {
1391 display: flex;
1392 align-items: flex-start;
1393 gap: 8px;
1394 font-size: 12px;
1395 cursor: pointer;
1396}
1397
1398.mcp-tool-item input[type="checkbox"] {
1399 margin-top: 2px;
1400 cursor: pointer;
1401}
1402
1403.mcp-tool-name {
1404 font-family: var(--font-mono, monospace);
1405 color: var(--text-primary);
1406 font-weight: 500;
1407}
1408
1409.mcp-tool-desc {
1410 color: var(--text-secondary);
1411 font-size: 11px;
1412 margin-left: 4px;
1413}
1414
1415.mcp-tools-empty {
1416 font-size: 11px;
1417 color: var(--text-secondary);
1418 font-style: italic;
1419 margin-top: 8px;
1420 padding: 8px;
1421 background: var(--bg-tertiary);
1422 border-radius: 4px;
1423 text-align: center;
1424}