Refine Browser naming and managed MCP UX

- Rename Browser Use surfaces to Browser
- Register Browser MCP under the new server name
- Mark CloudCLI-managed MCP servers read-only
- Adjust MCP stdio framing and sidebar footer sizing
This commit is contained in:
Simos Mikelatos
2026-06-17 19:18:23 +00:00
parent 9881e5e366
commit 59194d1502
17 changed files with 166 additions and 132 deletions

View File

@@ -53,7 +53,7 @@ async function callBrowserUseApi(toolName: string, input: Record<string, unknown
});
const data = await response.json() as { success?: boolean; data?: unknown; error?: string };
if (!response.ok || data.success === false) {
throw new Error(data.error || `Browser Use API request failed (${response.status})`);
throw new Error(data.error || `Browser API request failed (${response.status})`);
}
return data.data;
}
@@ -61,7 +61,7 @@ async function callBrowserUseApi(toolName: string, input: Record<string, unknown
const sessionIdSchema = {
type: 'object',
properties: {
sessionId: { type: 'string', description: 'Browser Use session id.' },
sessionId: { type: 'string', description: 'Browser session id.' },
},
required: ['sessionId'],
};
@@ -69,7 +69,7 @@ const sessionIdSchema = {
const tools: ToolDefinition[] = [
{
name: 'browser_create_session',
description: 'Create a temporary Browser Use session that the agent can control. Optionally provide a background profileName to reuse cookies and storage.',
description: 'Create a temporary Browser session that the agent can control. Optionally provide a background profileName to reuse cookies and storage.',
inputSchema: {
type: 'object',
properties: {
@@ -79,22 +79,22 @@ const tools: ToolDefinition[] = [
},
{
name: 'browser_list_sessions',
description: 'List Browser Use sessions currently available to agents.',
description: 'List Browser sessions currently available to agents.',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'browser_snapshot',
description: 'Capture current page metadata, screenshot data URL, and visible body text for a Browser Use session.',
description: 'Capture current page metadata, screenshot data URL, and visible body text for a Browser session.',
inputSchema: sessionIdSchema,
},
{
name: 'browser_take_screenshot',
description: 'Capture the latest screenshot for a Browser Use session.',
description: 'Capture the latest screenshot for a Browser session.',
inputSchema: sessionIdSchema,
},
{
name: 'browser_navigate',
description: 'Navigate a Browser Use session to an HTTP or HTTPS URL.',
description: 'Navigate a Browser session to an HTTP or HTTPS URL.',
inputSchema: {
type: 'object',
properties: {
@@ -196,7 +196,7 @@ const tools: ToolDefinition[] = [
},
{
name: 'browser_tabs',
description: 'List, open, select, or close tabs in a Browser Use session.',
description: 'List, open, select, or close tabs in a Browser session.',
inputSchema: {
type: 'object',
properties: {
@@ -210,7 +210,7 @@ const tools: ToolDefinition[] = [
},
{
name: 'browser_close_session',
description: 'Stop a Browser Use session controlled by agents.',
description: 'Stop a Browser session controlled by agents.',
inputSchema: sessionIdSchema,
},
];
@@ -302,7 +302,7 @@ async function handleMessage(message: JsonRpcRequest) {
return {
protocolVersion: '2024-11-05',
capabilities: { tools: {} },
serverInfo: { name: 'cloudcli-browser-use', version: '1.0.0' },
serverInfo: { name: 'cloudcli-browser', version: '1.0.0' },
};
}
@@ -327,8 +327,9 @@ async function handleMessage(message: JsonRpcRequest) {
}
function writeMessage(message: Record<string, unknown>) {
const payload = JSON.stringify(message);
process.stdout.write(`Content-Length: ${Buffer.byteLength(payload, 'utf8')}\r\n\r\n${payload}`);
// MCP stdio transport uses newline-delimited JSON (one JSON-RPC message per line,
// no embedded newlines). This is NOT the LSP Content-Length framing.
process.stdout.write(`${JSON.stringify(message)}\n`);
}
function sendResult(id: string | number | null | undefined, result: unknown) {
@@ -352,33 +353,18 @@ function sendError(id: string | number | null | undefined, error: unknown) {
});
}
let buffer = Buffer.alloc(0);
let buffer = '';
process.stdin.on('data', (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
while (true) {
const headerEnd = buffer.indexOf('\r\n\r\n');
if (headerEnd === -1) {
return;
}
const header = buffer.slice(0, headerEnd).toString('utf8');
const lengthMatch = /Content-Length:\s*(\d+)/i.exec(header);
if (!lengthMatch) {
buffer = buffer.slice(headerEnd + 4);
buffer += chunk.toString('utf8');
let newlineIndex: number;
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
const rawMessage = buffer.slice(0, newlineIndex).trim();
buffer = buffer.slice(newlineIndex + 1);
if (!rawMessage) {
continue;
}
const length = Number.parseInt(lengthMatch[1], 10);
const messageStart = headerEnd + 4;
const messageEnd = messageStart + length;
if (buffer.length < messageEnd) {
return;
}
const rawMessage = buffer.slice(messageStart, messageEnd).toString('utf8');
buffer = buffer.slice(messageEnd);
void (async () => {
let request: JsonRpcRequest;
try {

View File

@@ -8,7 +8,7 @@
* (no args) - Start the server (default)
* start - Start the server
* sandbox - Manage Docker sandbox environments
* browser-use-mcp - Run Browser Use MCP stdio server
* browser-use-mcp - Run Browser MCP stdio server
* status - Show configuration and data locations
* help - Show help information
* version - Show version information
@@ -157,7 +157,7 @@ Usage:
Commands:
start Start the CloudCLI server (default)
sandbox Manage Docker sandbox environments
browser-use-mcp Run the Browser Use MCP stdio server
browser-use-mcp Run the Browser MCP stdio server
status Show configuration and data locations
update Update to the latest version
help Show this help information

View File

@@ -196,10 +196,10 @@ app.use('/api/gemini', authenticateToken, geminiRoutes);
// Plugins API Routes (protected)
app.use('/api/plugins', authenticateToken, pluginsRoutes);
// Browser Use MCP bridge API (local token protected)
// Browser MCP bridge API (local token protected)
app.use('/api/browser-use-mcp', browserUseMcpRoutes);
// Browser Use API Routes (protected)
// Browser API Routes (protected)
app.use('/api/browser-use', authenticateToken, browserUseRoutes);
// Unified provider MCP routes (protected)
@@ -1717,7 +1717,7 @@ async function startServer() {
try {
await browserUseService.stopAllSessions();
} catch (err) {
console.error('[Browser Use] Error stopping sessions during shutdown:', err?.message || err);
console.error('[Browser] Error stopping sessions during shutdown:', err?.message || err);
}
try {
await stopAllPlugins();

View File

@@ -16,7 +16,7 @@ router.use((req, res, next) => {
const expected = browserUseService.getMcpToken();
const token = readBearerToken(req.headers.authorization) || String(req.headers['x-browser-use-mcp-token'] || '');
if (!token || token !== expected) {
res.status(401).json({ success: false, error: 'Invalid Browser Use MCP token.' });
res.status(401).json({ success: false, error: 'Invalid Browser MCP token.' });
return;
}
next();
@@ -104,7 +104,7 @@ router.post('/tools/:toolName', async (req, res) => {
result = await browserUseService.agentStopSession(sessionId);
break;
default:
res.status(404).json({ success: false, error: `Unknown Browser Use MCP tool "${toolName}".` });
res.status(404).json({ success: false, error: `Unknown Browser MCP tool "${toolName}".` });
return;
}
@@ -112,7 +112,7 @@ router.post('/tools/:toolName', async (req, res) => {
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Browser Use MCP tool failed.',
error: error instanceof Error ? error.message : 'Browser MCP tool failed.',
});
}
});

View File

@@ -14,7 +14,7 @@ router.get('/status', async (_req, res) => {
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Failed to load Browser Use status.',
error: error instanceof Error ? error.message : 'Failed to load Browser status.',
});
}
});
@@ -25,7 +25,7 @@ router.get('/settings', async (_req, res) => {
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Failed to load Browser Use settings.',
error: error instanceof Error ? error.message : 'Failed to load Browser settings.',
});
}
});
@@ -37,7 +37,7 @@ router.put('/settings', async (req, res) => {
} catch (error) {
res.status(400).json({
success: false,
error: error instanceof Error ? error.message : 'Failed to save Browser Use settings.',
error: error instanceof Error ? error.message : 'Failed to save Browser settings.',
});
}
});
@@ -53,7 +53,7 @@ router.post('/runtime/install', async (_req, res) => {
} catch (error) {
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : 'Failed to install Browser Use runtime.',
error: error instanceof Error ? error.message : 'Failed to install Browser runtime.',
});
}
});

View File

@@ -76,8 +76,8 @@ const DEFAULT_SETTINGS: BrowserUseSettings = {
};
const AGENT_OWNER_ID = 'agent';
const PROFILE_ROOT = path.join(os.homedir(), '.cloudcli', 'browser-use', 'profiles');
const MCP_SERVER_NAME = 'cloudcli-browser-use';
const MCP_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini', 'opencode'];
const MCP_SERVER_NAME = 'cloudcli-browser';
const LEGACY_MCP_SERVER_NAMES = ['cloudcli-browser-use'];
function getRuntime(): BrowserUseRuntime {
return IS_PLATFORM ? 'cloud' : 'local';
@@ -95,7 +95,7 @@ function readSettings(): BrowserUseSettings {
enabled: parsed.enabled === true,
};
} catch (error: any) {
console.warn('[Browser Use] Failed to read settings:', error?.message || error);
console.warn('[Browser] Failed to read settings:', error?.message || error);
return DEFAULT_SETTINGS;
}
}
@@ -121,7 +121,7 @@ function getOrCreateMcpToken(): string {
function getSetupMessage(settings: BrowserUseSettings, readiness: RuntimeReadiness): string {
if (!settings.enabled) {
return 'Browser Use is disabled in settings.';
return 'Browser is disabled in settings.';
}
if (!readiness.playwrightInstalled) {
@@ -132,7 +132,7 @@ function getSetupMessage(settings: BrowserUseSettings, readiness: RuntimeReadine
return 'Playwright is installed, but Chromium is missing. Install the Chromium runtime to continue.';
}
return readiness.installMessage || 'Browser Use runtime is not ready.';
return readiness.installMessage || 'Browser runtime is not ready.';
}
function getPlaywright(): any | null {
@@ -164,6 +164,14 @@ function getMcpApiUrl(): string {
return `http://127.0.0.1:${port}/api/browser-use-mcp`;
}
async function removeMcpServerFromAllProviders(name: string) {
const results = await providerMcpService.removeMcpServerFromAllProviders({
name,
scope: 'user',
});
return results.map((result) => ({ ...result, name }));
}
function normalizeProfileName(profileName?: string | null): string | null {
const normalized = String(profileName || '').trim();
if (!normalized) {
@@ -259,7 +267,7 @@ function formatInstallError(error: unknown): string {
if (message.includes('sudo') && message.includes('password')) {
return 'Installing Chromium system dependencies requires administrator privileges. Run `npx playwright install-deps chromium` on the machine where CloudCLI runs, then try again.';
}
return message || 'Failed to install Browser Use runtime.';
return message || 'Failed to install Browser runtime.';
}
async function installRuntime(): Promise<{ success: boolean; message: string }> {
@@ -281,7 +289,7 @@ async function installRuntime(): Promise<{ success: boolean; message: string }>
lastInstallMessage = 'Installing Chromium runtime...';
await runCommand(npmCommand, ['exec', '--', 'playwright', 'install', 'chromium']);
lastInstallMessage = 'Browser Use runtime installed.';
lastInstallMessage = 'Browser runtime installed.';
return { success: true, message: lastInstallMessage };
} catch (error) {
lastInstallMessage = formatInstallError(error);
@@ -418,13 +426,14 @@ export const browserUseService = {
installInProgress: readiness.installInProgress,
sessionCount: sessions.size,
message: available
? 'Browser Use runtime is available.'
? 'Browser runtime is available.'
: getSetupMessage(settings, readiness),
};
},
async registerAgentMcp() {
const { command, args } = getMcpCommand();
await Promise.all(LEGACY_MCP_SERVER_NAMES.map((name) => removeMcpServerFromAllProviders(name)));
const results = await providerMcpService.addMcpServerToAllProviders({
name: MCP_SERVER_NAME,
scope: 'user',
@@ -444,21 +453,9 @@ export const browserUseService = {
},
async unregisterAgentMcp() {
const results = await Promise.all(MCP_PROVIDERS.map(async (provider) => {
try {
const result = await providerMcpService.removeProviderMcpServer(provider, {
name: MCP_SERVER_NAME,
scope: 'user',
});
return { provider, removed: result.removed };
} catch (error) {
return {
provider,
removed: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}));
const results = (await Promise.all(
[MCP_SERVER_NAME, ...LEGACY_MCP_SERVER_NAMES].map((name) => removeMcpServerFromAllProviders(name)),
)).flat();
return { name: MCP_SERVER_NAME, results };
},
@@ -480,7 +477,7 @@ export const browserUseService = {
async createAgentSession(options?: { profileName?: string | null }) {
const settings = readSettings();
if (!settings.enabled) {
throw new Error('Browser Use agent tools are disabled.');
throw new Error('Browser agent tools are disabled.');
}
await expireStaleSessions();
@@ -507,7 +504,7 @@ export const browserUseService = {
const activeOwnerSessions = ownerSessions(AGENT_OWNER_ID).filter((item) => item.status === 'ready');
if (activeOwnerSessions.length >= MAX_SESSIONS_PER_OWNER) {
throw new Error(`Browser Use is limited to ${MAX_SESSIONS_PER_OWNER} active agent sessions.`);
throw new Error(`Browser is limited to ${MAX_SESSIONS_PER_OWNER} active agent sessions.`);
}
const readiness = getRuntimeReadiness();
@@ -563,7 +560,7 @@ export const browserUseService = {
async getAgentSession(sessionId: string) {
const settings = readSettings();
if (!settings.enabled) {
throw new Error('Browser Use agent tools are disabled.');
throw new Error('Browser agent tools are disabled.');
}
const session = sessions.get(sessionId);
if (!session || session.ownerId !== AGENT_OWNER_ID) {

View File

@@ -3,7 +3,7 @@ import test from 'node:test';
import { browserUseService } from '@/modules/browser-use/browser-use.service.js';
test('browser use monitor list starts empty without agent sessions', async () => {
test('browser monitor list starts empty without agent sessions', async () => {
const sessions = await browserUseService.listSessions();
assert.deepEqual(sessions, []);

View File

@@ -80,4 +80,30 @@ export const providerMcpService = {
return results;
},
/**
* Removes one MCP server from every provider. Mirrors `addMcpServerToAllProviders`
* by iterating the live provider registry, so callers stay in sync with which
* providers exist instead of maintaining their own provider list.
*/
async removeMcpServerFromAllProviders(
input: { name: string; scope?: McpScope; workspacePath?: string },
): Promise<Array<{ provider: LLMProvider; removed: boolean; error?: string }>> {
const results: Array<{ provider: LLMProvider; removed: boolean; error?: string }> = [];
const providers = providerRegistry.listProviders();
for (const provider of providers) {
try {
const result = await provider.mcp.removeServer(input);
results.push({ provider: provider.id, removed: result.removed });
} catch (error) {
results.push({
provider: provider.id,
removed: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
return results;
},
};