From 044d628a47f063bcbbd9adba7860542156a0c66e Mon Sep 17 00:00:00 2001
From: Clawd <ai@clawd.bot>
Date: Sun, 1 Mar 2026 10:15:14 -0800
Subject: feat(mcp): add tool discovery and per-tool permissions

- Add MCP protocol client for tool discovery (initialize + tools/list)
- Show discovered tools in settings UI with enable/disable checkboxes
- Build explicit allowedTools list from enabled MCP tools
- Remove bypassPermissions hack - now uses proper tool allowlisting
- Format: mcp__servername__toolname for SDK allowedTools
---
 renderer/src/components/settings/McpSettings.tsx | 200 ++++++++++++++++++-----
 1 file changed, 163 insertions(+), 37 deletions(-)

(limited to 'renderer/src/components/settings/McpSettings.tsx')

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;
 
 type McpServerType = "stdio" | "sse" | "http";
 
+interface McpTool {
+  name: string;
+  description?: string;
+}
+
 interface McpServerConfig {
   type: McpServerType;
   command?: string;
@@ -11,6 +16,9 @@ interface McpServerConfig {
   env?: Record<string, string>;
   url?: string;
   headers?: Record<string, string>;
+  // Tool management
+  discoveredTools?: McpTool[];
+  enabledTools?: string[]; // If undefined/empty after discovery, all tools enabled
 }
 
 type McpServersConfig = Record<string, McpServerConfig>;
@@ -33,6 +41,7 @@ export function McpSettings() {
   const [editing, setEditing] = useState<EditingServer | null>(null);
   const [saveStatus, setSaveStatus] = useState<"idle" | "saved" | "error">("idle");
   const [error, setError] = useState<string | null>(null);
+  const [discovering, setDiscovering] = useState<string | null>(null); // server name being discovered
 
   useEffect(() => {
     api.getSettings(["mcpServers"]).then((settings) => {
@@ -70,6 +79,62 @@ export function McpSettings() {
     await saveServers(rest);
   };
 
+  const handleDiscoverTools = async (name: string) => {
+    if (!servers) return;
+    const config = servers[name];
+    
+    setDiscovering(name);
+    setError(null);
+    
+    try {
+      const result = await api.discoverMcpTools({
+        type: config.type,
+        command: config.command,
+        args: config.args,
+        env: config.env,
+        url: config.url,
+      });
+      
+      if (result.error) {
+        setError(`Discovery failed: ${result.error}`);
+      } else {
+        // Save discovered tools, enable all by default
+        const newServers = {
+          ...servers,
+          [name]: {
+            ...config,
+            discoveredTools: result.tools,
+            enabledTools: result.tools.map((t) => t.name),
+          },
+        };
+        await saveServers(newServers);
+      }
+    } catch (e) {
+      setError(`Discovery failed: ${String(e)}`);
+    } finally {
+      setDiscovering(null);
+    }
+  };
+
+  const handleToggleTool = async (serverName: string, toolName: string, enabled: boolean) => {
+    if (!servers) return;
+    const config = servers[serverName];
+    const currentEnabled = config.enabledTools || config.discoveredTools?.map((t) => t.name) || [];
+    
+    const newEnabled = enabled
+      ? [...currentEnabled, toolName]
+      : currentEnabled.filter((t) => t !== toolName);
+    
+    const newServers = {
+      ...servers,
+      [serverName]: {
+        ...config,
+        enabledTools: newEnabled,
+      },
+    };
+    await saveServers(newServers);
+  };
+
   const handleSaveEdit = async () => {
     if (!editing || !servers) return;
     const { originalName, name, config } = editing;
@@ -113,6 +178,21 @@ export function McpSettings() {
       }
     }
 
+    // Preserve discovered tools if config didn't change substantially
+    const oldConfig = originalName ? servers[originalName] : null;
+    if (oldConfig?.discoveredTools) {
+      const configChanged =
+        config.type !== oldConfig.type ||
+        config.command !== oldConfig.command ||
+        JSON.stringify(config.args) !== JSON.stringify(oldConfig.args) ||
+        config.url !== oldConfig.url;
+      
+      if (!configChanged) {
+        cleanConfig.discoveredTools = oldConfig.discoveredTools;
+        cleanConfig.enabledTools = oldConfig.enabledTools;
+      }
+    }
+
     newServers[name.trim()] = cleanConfig;
     await saveServers(newServers);
     setEditing(null);
@@ -139,13 +219,11 @@ export function McpSettings() {
       <div className="settings-section-title">MCP Servers</div>
       <div className="settings-section-desc">
         Configure Model Context Protocol servers to give Claude access to
-        external data sources like Jira, documentation, or databases. Servers
-        are available in all phases.
+        external data sources. Click "Discover Tools" to fetch available tools
+        and select which ones to enable.
       </div>
 
-      {error && (
-        <div className="mcp-error">{error}</div>
-      )}
+      {error && <div className="mcp-error">{error}</div>}
 
       {/* Server List */}
       {!editing && (
@@ -154,39 +232,87 @@ export function McpSettings() {
             {serverNames.length === 0 ? (
               <div className="mcp-empty">No MCP servers configured</div>
             ) : (
-              serverNames.map((name) => (
-                <div key={name} className="mcp-server-card">
-                  <div className="mcp-server-info">
-                    <span className="mcp-server-name">{name}</span>
-                    <span className="mcp-server-type">{servers[name].type}</span>
-                    <span className="mcp-server-detail">
-                      {servers[name].type === "stdio"
-                        ? servers[name].command
-                        : servers[name].url}
-                    </span>
+              serverNames.map((name) => {
+                const config = servers[name];
+                const hasTools = config.discoveredTools && config.discoveredTools.length > 0;
+                const enabledTools = config.enabledTools || config.discoveredTools?.map((t) => t.name) || [];
+                
+                return (
+                  <div key={name} className="mcp-server-card">
+                    <div className="mcp-server-header">
+                      <div className="mcp-server-info">
+                        <span className="mcp-server-name">{name}</span>
+                        <span className="mcp-server-type">{config.type}</span>
+                        <span className="mcp-server-detail">
+                          {config.type === "stdio" ? config.command : config.url}
+                        </span>
+                      </div>
+                      <div className="mcp-server-actions">
+                        <button
+                          className="btn-secondary btn-small"
+                          onClick={() => handleDiscoverTools(name)}
+                          disabled={discovering === name}
+                        >
+                          {discovering === name ? "Discovering..." : "Discover Tools"}
+                        </button>
+                        <button
+                          className="btn-secondary btn-small"
+                          onClick={() =>
+                            setEditing({
+                              originalName: name,
+                              name,
+                              config: { ...config },
+                            })
+                          }
+                        >
+                          Edit
+                        </button>
+                        <button
+                          className="btn-secondary btn-small btn-danger"
+                          onClick={() => handleDelete(name)}
+                        >
+                          Delete
+                        </button>
+                      </div>
+                    </div>
+                    
+                    {/* Tools Section */}
+                    {hasTools && (
+                      <div className="mcp-tools-section">
+                        <div className="mcp-tools-header">
+                          Tools ({enabledTools.length}/{config.discoveredTools!.length} enabled)
+                        </div>
+                        <div className="mcp-tools-list">
+                          {config.discoveredTools!.map((tool) => {
+                            const isEnabled = enabledTools.includes(tool.name);
+                            return (
+                              <label key={tool.name} className="mcp-tool-item">
+                                <input
+                                  type="checkbox"
+                                  checked={isEnabled}
+                                  onChange={(e) =>
+                                    handleToggleTool(name, tool.name, e.target.checked)
+                                  }
+                                />
+                                <span className="mcp-tool-name">{tool.name}</span>
+                                {tool.description && (
+                                  <span className="mcp-tool-desc">{tool.description}</span>
+                                )}
+                              </label>
+                            );
+                          })}
+                        </div>
+                      </div>
+                    )}
+                    
+                    {!hasTools && (
+                      <div className="mcp-tools-empty">
+                        Click "Discover Tools" to see available tools
+                      </div>
+                    )}
                   </div>
-                  <div className="mcp-server-actions">
-                    <button
-                      className="btn-secondary btn-small"
-                      onClick={() =>
-                        setEditing({
-                          originalName: name,
-                          name,
-                          config: { ...servers[name] },
-                        })
-                      }
-                    >
-                      Edit
-                    </button>
-                    <button
-                      className="btn-secondary btn-small btn-danger"
-                      onClick={() => handleDelete(name)}
-                    >
-                      Delete
-                    </button>
-                  </div>
-                </div>
-              ))
+                );
+              })
             )}
           </div>
 
-- 
cgit v1.2.3

