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