feat: run claude as non-root for bypassPermissions + replace MCP menu with live tools view

This commit is contained in:
Zac Gaetano 2026-04-05 12:26:37 -04:00
parent 318e7f01ad
commit 1e171dff1f

View file

@ -55,10 +55,8 @@ const App = () => {
const [editingTaskId, setEditingTaskId] = useState(null);
const [taskRuns, setTaskRuns] = useState({});
// MCP state
const [mcpServers, setMcpServers] = useState([]);
const [showMcpForm, setShowMcpForm] = useState(false);
const [mcpForm, setMcpForm] = useState({ name: '', server_type: 'sse', url: '', command: '' });
// Connected tools state (read-only, populated from Claude's system/init)
const [claudeTools, setClaudeTools] = useState(null);
// ---- Data fetching ----
const fetchTasks = async () => {
@ -68,10 +66,10 @@ const App = () => {
} catch (e) { /* ignore */ }
};
const fetchMcpServers = async () => {
const fetchClaudeTools = async () => {
try {
const r = await fetch(`${API}/api/mcp/servers`);
if (r.ok) setMcpServers(await r.json());
const r = await fetch(`${API}/api/claude/tools`);
if (r.ok) setClaudeTools(await r.json());
} catch (e) { /* ignore */ }
};
@ -95,12 +93,13 @@ const App = () => {
useEffect(() => {
fetchTasks();
fetchMcpServers();
fetchClaudeTools();
fetchChatSessions();
fetchClaudeStatus();
const interval = setInterval(() => {
fetchTasks();
fetchClaudeStatus();
fetchClaudeTools();
}, 8000);
return () => clearInterval(interval);
}, []);
@ -468,9 +467,9 @@ const App = () => {
onClick={() => { setActiveView('tasks'); fetchTasks(); }}>
<span className="nav-icon">📋</span> Tasks
</button>
<button className={`nav-item ${activeView === 'mcp' ? 'active' : ''}`}
onClick={() => { setActiveView('mcp'); fetchMcpServers(); }}>
<span className="nav-icon">🔌</span> MCP Servers
<button className={`nav-item ${activeView === 'tools' ? 'active' : ''}`}
onClick={() => { setActiveView('tools'); fetchClaudeTools(); }}>
<span className="nav-icon">🔌</span> Tools
</button>
</nav>
@ -758,86 +757,58 @@ const App = () => {
</div>
)}
{/* ====== MCP VIEW ====== */}
{activeView === 'mcp' && (
{/* ====== TOOLS VIEW ====== */}
{activeView === 'tools' && (
<div className="view-container">
<div className="view-header">
<h2>MCP Servers</h2>
<button className="btn btn-primary" onClick={() => setShowMcpForm(true)}>
+ Add Server
</button>
<h2>Connected Tools</h2>
<button className="btn btn-secondary btn-sm" onClick={fetchClaudeTools}> Refresh</button>
</div>
<p className="view-desc">
Configure MCP servers that Claude can use as tools. These are saved and
passed to Claude via its config. Restart Claude after adding servers.
Tools available in the current Claude session synced automatically from your account.
{claudeTools && ` ${claudeTools.total} tools across ${Object.keys(claudeTools.mcp_servers || {}).length} MCP servers.`}
</p>
{showMcpForm && (
<div className="form-card">
<h3>Add MCP Server</h3>
<div className="form-grid">
<div className="form-field">
<label>Name</label>
<input value={mcpForm.name}
onChange={e => setMcpForm(p => ({ ...p, name: e.target.value }))}
placeholder="my-server" />
{!claudeTools || claudeTools.total === 0 ? (
<div className="empty-state">
{claudeTools === null
? 'Loading tools…'
: 'No tools detected yet. Start Claude and wait for initialization.'}
</div>
<div className="form-field">
<label>Type</label>
<select value={mcpForm.server_type}
onChange={e => setMcpForm(p => ({ ...p, server_type: e.target.value }))}>
<option value="sse">SSE (HTTP)</option>
<option value="stdio">stdio (command)</option>
</select>
</div>
{mcpForm.server_type === 'sse' && (
<div className="form-field form-field-full">
<label>URL</label>
<input value={mcpForm.url}
onChange={e => setMcpForm(p => ({ ...p, url: e.target.value }))}
placeholder="https://mcp.example.com/sse" />
</div>
)}
{mcpForm.server_type === 'stdio' && (
<div className="form-field form-field-full">
<label>Command</label>
<input value={mcpForm.command}
onChange={e => setMcpForm(p => ({ ...p, command: e.target.value }))}
placeholder="npx -y @my/mcp-server" />
</div>
)}
</div>
<div className="form-actions">
<button className="btn btn-primary" onClick={saveMcp}
disabled={!mcpForm.name}>
Add Server
</button>
<button className="btn btn-secondary" onClick={() => setShowMcpForm(false)}>
Cancel
</button>
</div>
</div>
)}
) : (
<div className="mcp-list">
{mcpServers.length === 0 && (
<div className="empty-state">No MCP servers configured.</div>
)}
{mcpServers.map(s => (
<div key={s.name} className="mcp-card">
{/* Built-in tools */}
{claudeTools.builtin_tools?.length > 0 && (
<div className="mcp-card">
<div className="mcp-card-header">
<div>
<span className="mcp-name">{s.name}</span>
<span className="mcp-type-badge">{s.type || 'sse'}</span>
<span className="mcp-name">Built-in Tools</span>
<span className="mcp-type-badge">{claudeTools.builtin_tools.length} tools</span>
</div>
<button className="btn btn-sm btn-danger" onClick={() => removeMcp(s.name)}>
Remove
</button>
</div>
<div className="mcp-url">{s.url || s.command || ''}</div>
<div className="mcp-url">{claudeTools.builtin_tools.join(', ')}</div>
</div>
)}
{/* MCP servers */}
{Object.entries(claudeTools.mcp_servers || {}).map(([server, count]) => (
<div key={server} className="mcp-card">
<div className="mcp-card-header">
<div>
<span className="mcp-name">{server}</span>
<span className="mcp-type-badge">{count} tools</span>
<span className="task-status-badge status-idle" style={{marginLeft: '6px'}}> connected</span>
</div>
</div>
<div className="mcp-url">
{(claudeTools.raw || [])
.filter(t => t.name?.startsWith(`mcp__${server}__`))
.map(t => t.name.replace(`mcp__${server}__`, ''))
.join(', ')}
</div>
</div>
))}
</div>
)}
</div>
)}