feat: expose browser use to agents via MCP

This commit is contained in:
Simos Mikelatos
2026-06-15 19:47:58 +00:00
parent 6e7e2ff4c1
commit 0426522406
8 changed files with 1030 additions and 17 deletions

View File

@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Download, ExternalLink, Globe, Loader2, MonitorPlay, Navigation, Pause, RefreshCw, Square } from 'lucide-react';
import { Bot, Download, ExternalLink, Globe, Loader2, MonitorPlay, Navigation, Pause, RefreshCw, Share2, Square, X } from 'lucide-react';
import { Badge, Button } from '../../../shared/view/ui';
import { authenticatedFetch } from '../../../utils/api';
@@ -12,6 +12,7 @@ type BrowserUseStatus = {
chromiumInstalled: boolean;
installInProgress: boolean;
sessionCount: number;
agentToolsEnabled: boolean;
mcpRecommended: boolean;
message: string;
};
@@ -27,6 +28,9 @@ type BrowserUseSession = {
updatedAt: string;
lastAction: string | null;
message: string | null;
agentAccessEnabled: boolean;
createdBy: 'user' | 'agent';
profileName: string | null;
};
type BrowserUsePanelProps = {
@@ -112,6 +116,18 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
await readJson(response);
});
const grantAgentAccess = () => runAction(async () => {
if (!selectedSession) return;
const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}/agent-access/grant`, { method: 'POST' });
await readJson(response);
});
const revokeAgentAccess = () => runAction(async () => {
if (!selectedSession) return;
const response = await authenticatedFetch(`/api/browser-use/sessions/${selectedSession.id}/agent-access/revoke`, { method: 'POST' });
await readJson(response);
});
const installRuntime = () => runAction(async () => {
setIsInstalling(true);
try {
@@ -138,7 +154,7 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
)}
</div>
<p className="mt-0.5 text-xs text-muted-foreground">
Managed Playwright browser sessions with owner-scoped screenshots and navigation.
Create browser sessions, watch agent activity, and decide which sessions agents may control.
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
@@ -159,6 +175,11 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Runtime</div>
<div className="mt-2 text-sm text-foreground">{status?.available ? 'Available' : 'Setup required'}</div>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">{status?.message || 'Loading Browser Use status...'}</p>
{status?.enabled && (
<div className="mt-3 rounded-md border border-border/70 bg-background/60 px-2 py-2 text-xs text-muted-foreground">
Agent tools: {status.agentToolsEnabled ? 'enabled' : 'disabled in settings'}
</div>
)}
{canInstallRuntime && (
<Button
type="button"
@@ -178,6 +199,11 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
</div>
<div className="mt-3 space-y-2">
<div className="rounded-lg border border-border/70 bg-muted/30 p-3 text-xs leading-relaxed text-muted-foreground">
Agents can create their own browser sessions when browser tools are enabled. Use
<span className="font-medium text-foreground"> Give Agent Access </span>
to let agents control a session you created, and revoke access whenever you want.
</div>
{sessions.map((session) => (
<button
key={session.id}
@@ -192,6 +218,19 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
<span className="truncate font-medium">{session.title || session.url || 'Browser session'}</span>
<Badge variant="outline" className="text-[10px]">{session.status}</Badge>
</div>
<div className="mt-1 flex flex-wrap gap-1">
{session.createdBy === 'agent' && (
<span className="rounded border border-primary/30 px-1.5 py-0.5 text-[10px] text-primary">agent</span>
)}
{session.agentAccessEnabled && (
<span className="rounded border border-emerald-500/30 px-1.5 py-0.5 text-[10px] text-emerald-600 dark:text-emerald-300">
shared
</span>
)}
{session.profileName && (
<span className="rounded border border-border px-1.5 py-0.5 text-[10px]">profile: {session.profileName}</span>
)}
</div>
<div className="mt-1 truncate text-xs">{session.url || session.message || session.id}</div>
</button>
))}
@@ -215,6 +254,17 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
<Navigation className="h-4 w-4" />
Go
</Button>
{selectedSession?.agentAccessEnabled ? (
<Button variant="outline" size="sm" onClick={revokeAgentAccess} disabled={isBusy || !selectedSession}>
<X className="h-4 w-4" />
Revoke Agent
</Button>
) : (
<Button variant="outline" size="sm" onClick={grantAgentAccess} disabled={isBusy || !selectedSession || !status?.agentToolsEnabled}>
<Share2 className="h-4 w-4" />
Give Agent Access
</Button>
)}
<Button variant="outline" size="sm" disabled>
<Pause className="h-4 w-4" />
Pause
@@ -236,6 +286,12 @@ export default function BrowserUsePanel({ isVisible }: BrowserUsePanelProps) {
<div className="flex items-center gap-2 border-b border-border/60 px-3 py-2 text-xs text-muted-foreground">
<ExternalLink className="h-3.5 w-3.5" />
<span className="truncate">{selectedSession?.url || 'No page loaded'}</span>
{selectedSession?.agentAccessEnabled && (
<span className="ml-auto inline-flex items-center gap-1 rounded border border-emerald-500/30 px-2 py-0.5 text-emerald-600 dark:text-emerald-300">
<Bot className="h-3.5 w-3.5" />
Agent access active
</span>
)}
</div>
<div className="flex min-h-[360px] flex-1 items-center justify-center bg-neutral-950">
{selectedSession?.screenshotDataUrl ? (

View File

@@ -10,6 +10,7 @@ import SettingsToggle from '../../SettingsToggle';
type BrowserUseSettings = {
enabled: boolean;
agentToolsEnabled: boolean;
};
type BrowserUseStatus = {
@@ -19,6 +20,7 @@ type BrowserUseStatus = {
playwrightInstalled: boolean;
chromiumInstalled: boolean;
installInProgress: boolean;
agentToolsEnabled: boolean;
message: string;
};
@@ -31,7 +33,7 @@ async function readJson<T>(response: Response): Promise<T> {
}
export default function BrowserUseSettingsTab() {
const [settings, setSettings] = useState<BrowserUseSettings>({ enabled: false });
const [settings, setSettings] = useState<BrowserUseSettings>({ enabled: false, agentToolsEnabled: false });
const [status, setStatus] = useState<BrowserUseStatus | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
@@ -57,13 +59,13 @@ export default function BrowserUseSettingsTab() {
.finally(() => setIsLoading(false));
}, [loadState]);
const updateEnabled = async (enabled: boolean) => {
const updateSettings = async (nextSettings: Partial<BrowserUseSettings>) => {
setIsSaving(true);
setError(null);
try {
const response = await authenticatedFetch('/api/browser-use/settings', {
method: 'PUT',
body: JSON.stringify({ enabled }),
body: JSON.stringify(nextSettings),
});
const data = await readJson<{ data: { settings: BrowserUseSettings } }>(response);
setSettings(data.data.settings);
@@ -105,12 +107,24 @@ export default function BrowserUseSettingsTab() {
>
<SettingsToggle
checked={settings.enabled}
onChange={(value) => void updateEnabled(value)}
onChange={(value) => void updateSettings({ enabled: value })}
ariaLabel="Enable Browser Use"
disabled={isLoading || isSaving}
/>
</SettingsRow>
<SettingsRow
label="Enable Browser Tools for Agents"
description="Register the Browser Use MCP server for all agent providers. Agents can create browser sessions and control sessions shared with agents."
>
<SettingsToggle
checked={settings.agentToolsEnabled}
onChange={(value) => void updateSettings({ agentToolsEnabled: value })}
ariaLabel="Enable Browser Tools for Agents"
disabled={isLoading || isSaving || !settings.enabled}
/>
</SettingsRow>
<div className="space-y-4 px-4 py-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0 space-y-1">