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 [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" />
|
||||
</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" />
|
||||
{!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="mcp-list">
|
||||
{/* Built-in tools */}
|
||||
{claudeTools.builtin_tools?.length > 0 && (
|
||||
<div className="mcp-card">
|
||||
<div className="mcp-card-header">
|
||||
<div>
|
||||
<span className="mcp-name">Built-in Tools</span>
|
||||
<span className="mcp-type-badge">{claudeTools.builtin_tools.length} tools</span>
|
||||
</div>
|
||||
</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 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>
|
||||
<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 className="mcp-url">
|
||||
{(claudeTools.raw || [])
|
||||
.filter(t => t.name?.startsWith(`mcp__${server}__`))
|
||||
.map(t => t.name.replace(`mcp__${server}__`, ''))
|
||||
.join(', ')}
|
||||
</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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Reference in a new issue