Add browser CDP MCP server with session management (#11844)

*  feat: add CDP browser MCP server

* ♻️ refactor: add navigation timeout for browser cdp

* 🐛 fix: reuse window for execute and add debugger logging

*  feat: add show option and multiline execute for browser cdp

*  feat: support multiple sessions for browser cdp

* ♻️ refactor: add LRU and idle cleanup for browser cdp sessions

* Refactor browser-cdp for readability and set Firefox UA

* 🐛 fix: type electron mock for cdp tests

* ♻️ refactor: rename browser_cdp MCP server to browser

Simplify the MCP server name from @cherry/browser-cdp to just browser
for cleaner tool naming in the MCP protocol.

*  feat: add fetch tool to browser MCP server

Add a new `fetch` tool that uses the CDP-controlled browser to fetch URLs
and return content in various formats (html, txt, markdown, json).

Also ignore .conductor folder in biome and eslint configs.

* ♻️ refactor: split browser MCP server into modular folder structure

Reorganize browser.ts (525 lines) into browser/ folder with separate
files for better maintainability. Each tool now has its own file with
schema, definition, and handler.

* ♻️ refactor: use switch statement in browser server request handler

* ♻️ refactor: extract helpers and use handler registry pattern

- Add successResponse/errorResponse helpers in tools/utils.ts
- Add closeWindow helper to consolidate window cleanup logic
- Add ensureDebuggerAttached helper to consolidate debugger setup
- Add toolHandlers map for registry-based handler lookup
- Simplify server.ts to use dynamic handler dispatch

* 🐛 fix: improve browser MCP server robustness

- Add try-catch for JSON.parse in fetch() to handle invalid JSON gracefully
- Add Zod schema validation to reset tool for consistency with other tools
- Fix memory leak in open() by ensuring event listeners cleanup on timeout
- Add JSDoc comments for key methods and classes

* ♻️ refactor: rename browser MCP to @cherry/browser

Follow naming convention of other builtin MCP servers.

* 🌐 i18n: translate pending strings across 8 locales

Translate all "[to be translated]" markers including:
- CDP browser MCP server description (all 8 locales)
- "Extra High" reasoning chain length option (6 locales)
- Git Bash configuration strings (el-gr, ja-jp)
This commit is contained in:
LiuVaayne 2025-12-16 09:29:30 +08:00 committed by GitHub
parent aeebd343d7
commit d41229c69b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 765 additions and 25 deletions

View File

@ -23,7 +23,7 @@
},
"files": {
"ignoreUnknown": false,
"includes": ["**", "!**/.claude/**", "!**/.vscode/**"],
"includes": ["**", "!**/.claude/**", "!**/.vscode/**", "!**/.conductor/**"],
"maxSize": 2097152
},
"formatter": {

View File

@ -61,6 +61,7 @@ export default defineConfig([
'tests/**',
'.yarn/**',
'.gitignore',
'.conductor/**',
'scripts/cloudflare-worker.js',
'src/main/integration/nutstore/sso/lib/**',
'src/main/integration/cherryai/index.js',

View File

@ -0,0 +1,134 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('electron', () => {
const sendCommand = vi.fn(async (command: string, params?: { expression?: string }) => {
if (command === 'Runtime.evaluate') {
if (params?.expression === 'document.documentElement.outerHTML') {
return { result: { value: '<html><body><h1>Test</h1><p>Content</p></body></html>' } }
}
if (params?.expression === 'document.body.innerText') {
return { result: { value: 'Test\nContent' } }
}
return { result: { value: 'ok' } }
}
return {}
})
const debuggerObj = {
isAttached: vi.fn(() => true),
attach: vi.fn(),
detach: vi.fn(),
sendCommand
}
const webContents = {
debugger: debuggerObj,
setUserAgent: vi.fn(),
getURL: vi.fn(() => 'https://example.com/'),
getTitle: vi.fn(async () => 'Example Title'),
once: vi.fn(),
removeListener: vi.fn(),
on: vi.fn()
}
const loadURL = vi.fn(async () => {})
const windows: any[] = []
class MockBrowserWindow {
private destroyed = false
public webContents = webContents
public loadURL = loadURL
public isDestroyed = vi.fn(() => this.destroyed)
public close = vi.fn(() => {
this.destroyed = true
})
public destroy = vi.fn(() => {
this.destroyed = true
})
public on = vi.fn()
constructor() {
windows.push(this)
}
}
const app = {
isReady: vi.fn(() => true),
whenReady: vi.fn(async () => {}),
on: vi.fn()
}
return {
BrowserWindow: MockBrowserWindow as any,
app,
__mockDebugger: debuggerObj,
__mockSendCommand: sendCommand,
__mockLoadURL: loadURL,
__mockWindows: windows
}
})
import * as electron from 'electron'
const { __mockWindows } = electron as typeof electron & { __mockWindows: any[] }
import { CdpBrowserController } from '../browser'
describe('CdpBrowserController', () => {
it('executes single-line code via Runtime.evaluate', async () => {
const controller = new CdpBrowserController()
const result = await controller.execute('1+1')
expect(result).toBe('ok')
})
it('opens a URL (hidden) and returns current page info', async () => {
const controller = new CdpBrowserController()
const result = await controller.open('https://foo.bar/', 5000, false)
expect(result.currentUrl).toBe('https://example.com/')
expect(result.title).toBe('Example Title')
})
it('opens a URL (visible) when show=true', async () => {
const controller = new CdpBrowserController()
const result = await controller.open('https://foo.bar/', 5000, true, 'session-a')
expect(result.currentUrl).toBe('https://example.com/')
expect(result.title).toBe('Example Title')
})
it('reuses session for execute and supports multiline', async () => {
const controller = new CdpBrowserController()
await controller.open('https://foo.bar/', 5000, false, 'session-b')
const result = await controller.execute('const a=1; const b=2; a+b;', 5000, 'session-b')
expect(result).toBe('ok')
})
it('evicts least recently used session when exceeding maxSessions', async () => {
const controller = new CdpBrowserController({ maxSessions: 2, idleTimeoutMs: 1000 * 60 })
await controller.open('https://foo.bar/', 5000, false, 's1')
await controller.open('https://foo.bar/', 5000, false, 's2')
await controller.open('https://foo.bar/', 5000, false, 's3')
const destroyedCount = __mockWindows.filter(
(w: any) => w.destroy.mock.calls.length > 0 || w.close.mock.calls.length > 0
).length
expect(destroyedCount).toBeGreaterThanOrEqual(1)
})
it('fetches URL and returns html format', async () => {
const controller = new CdpBrowserController()
const result = await controller.fetch('https://example.com/', 'html')
expect(result).toBe('<html><body><h1>Test</h1><p>Content</p></body></html>')
})
it('fetches URL and returns txt format', async () => {
const controller = new CdpBrowserController()
const result = await controller.fetch('https://example.com/', 'txt')
expect(result).toBe('Test\nContent')
})
it('fetches URL and returns markdown format (default)', async () => {
const controller = new CdpBrowserController()
const result = await controller.fetch('https://example.com/')
expect(typeof result).toBe('string')
expect(result).toContain('Test')
})
})

View File

@ -0,0 +1,307 @@
import { app, BrowserWindow } from 'electron'
import TurndownService from 'turndown'
import { logger, userAgent } from './types'
/**
* Controller for managing browser windows via Chrome DevTools Protocol (CDP).
* Supports multiple sessions with LRU eviction and idle timeout cleanup.
*/
export class CdpBrowserController {
private windows: Map<string, { win: BrowserWindow; lastActive: number }> = new Map()
private readonly maxSessions: number
private readonly idleTimeoutMs: number
constructor(options?: { maxSessions?: number; idleTimeoutMs?: number }) {
this.maxSessions = options?.maxSessions ?? 5
this.idleTimeoutMs = options?.idleTimeoutMs ?? 5 * 60 * 1000
}
private async ensureAppReady() {
if (!app.isReady()) {
await app.whenReady()
}
}
private touch(sessionId: string) {
const entry = this.windows.get(sessionId)
if (entry) entry.lastActive = Date.now()
}
private closeWindow(win: BrowserWindow, sessionId: string) {
try {
if (!win.isDestroyed()) {
if (win.webContents.debugger.isAttached()) {
win.webContents.debugger.detach()
}
win.close()
}
} catch (error) {
logger.warn('Error closing window', { error, sessionId })
}
}
private async ensureDebuggerAttached(dbg: Electron.Debugger, sessionId: string) {
if (!dbg.isAttached()) {
try {
logger.info('Attaching debugger', { sessionId })
dbg.attach('1.3')
await dbg.sendCommand('Page.enable')
await dbg.sendCommand('Runtime.enable')
logger.info('Debugger attached and domains enabled')
} catch (error) {
logger.error('Failed to attach debugger', { error })
throw error
}
}
}
private sweepIdle() {
const now = Date.now()
for (const [id, entry] of this.windows.entries()) {
if (now - entry.lastActive > this.idleTimeoutMs) {
this.closeWindow(entry.win, id)
this.windows.delete(id)
}
}
}
private evictIfNeeded(newSessionId: string) {
if (this.windows.size < this.maxSessions) return
let lruId: string | null = null
let lruTime = Number.POSITIVE_INFINITY
for (const [id, entry] of this.windows.entries()) {
if (id === newSessionId) continue
if (entry.lastActive < lruTime) {
lruTime = entry.lastActive
lruId = id
}
}
if (lruId) {
const entry = this.windows.get(lruId)
if (entry) {
this.closeWindow(entry.win, lruId)
}
this.windows.delete(lruId)
logger.info('Evicted session to respect maxSessions', { evicted: lruId })
}
}
private async getWindow(sessionId = 'default', forceNew = false, show = false): Promise<BrowserWindow> {
await this.ensureAppReady()
this.sweepIdle()
const existing = this.windows.get(sessionId)
if (existing && !existing.win.isDestroyed() && !forceNew) {
this.touch(sessionId)
return existing.win
}
if (existing && !existing.win.isDestroyed() && forceNew) {
try {
if (existing.win.webContents.debugger.isAttached()) {
existing.win.webContents.debugger.detach()
}
} catch (error) {
logger.warn('Error detaching debugger before recreate', { error, sessionId })
}
existing.win.destroy()
this.windows.delete(sessionId)
}
this.evictIfNeeded(sessionId)
const win = new BrowserWindow({
show,
webPreferences: {
contextIsolation: true,
sandbox: true,
nodeIntegration: false,
devTools: true
}
})
// Use a standard Chrome UA to avoid some anti-bot blocks
win.webContents.setUserAgent(userAgent)
// Log navigation lifecycle to help diagnose slow loads
win.webContents.on('did-start-loading', () => logger.info(`did-start-loading`, { sessionId }))
win.webContents.on('dom-ready', () => logger.info(`dom-ready`, { sessionId }))
win.webContents.on('did-finish-load', () => logger.info(`did-finish-load`, { sessionId }))
win.webContents.on('did-fail-load', (_e, code, desc) => logger.warn('Navigation failed', { code, desc }))
win.on('closed', () => {
this.windows.delete(sessionId)
})
this.windows.set(sessionId, { win, lastActive: Date.now() })
return win
}
/**
* Opens a URL in a browser window and waits for navigation to complete.
* @param url - The URL to navigate to
* @param timeout - Navigation timeout in milliseconds (default: 10000)
* @param show - Whether to show the browser window (default: false)
* @param sessionId - Session identifier for window reuse (default: 'default')
* @returns Object containing the current URL and page title after navigation
*/
public async open(url: string, timeout = 10000, show = false, sessionId = 'default') {
const win = await this.getWindow(sessionId, true, show)
logger.info('Loading URL', { url, sessionId })
const { webContents } = win
this.touch(sessionId)
// Track resolution state to prevent multiple handlers from firing
let resolved = false
let onFinish: () => void
let onDomReady: () => void
let onFail: (_event: Electron.Event, code: number, desc: string) => void
// Define cleanup outside Promise to ensure it's callable in finally block,
// preventing memory leaks when timeout occurs before navigation completes
const cleanup = () => {
webContents.removeListener('did-finish-load', onFinish)
webContents.removeListener('did-fail-load', onFail)
webContents.removeListener('dom-ready', onDomReady)
}
const loadPromise = new Promise<void>((resolve, reject) => {
onFinish = () => {
if (resolved) return
resolved = true
cleanup()
resolve()
}
onDomReady = () => {
if (resolved) return
resolved = true
cleanup()
resolve()
}
onFail = (_event: Electron.Event, code: number, desc: string) => {
if (resolved) return
resolved = true
cleanup()
reject(new Error(`Navigation failed (${code}): ${desc}`))
}
webContents.once('did-finish-load', onFinish)
webContents.once('dom-ready', onDomReady)
webContents.once('did-fail-load', onFail)
})
const timeoutPromise = new Promise<void>((_, reject) => {
setTimeout(() => reject(new Error('Navigation timed out')), timeout)
})
try {
await Promise.race([win.loadURL(url), loadPromise, timeoutPromise])
} finally {
// Always cleanup listeners to prevent memory leaks on timeout
cleanup()
}
const currentUrl = webContents.getURL()
const title = await webContents.getTitle()
return { currentUrl, title }
}
public async execute(code: string, timeout = 5000, sessionId = 'default') {
const win = await this.getWindow(sessionId)
this.touch(sessionId)
const dbg = win.webContents.debugger
await this.ensureDebuggerAttached(dbg, sessionId)
const evalPromise = dbg.sendCommand('Runtime.evaluate', {
expression: code,
awaitPromise: true,
returnByValue: true
})
const result = await Promise.race([
evalPromise,
new Promise((_, reject) => setTimeout(() => reject(new Error('Execution timed out')), timeout))
])
const evalResult = result as any
if (evalResult?.exceptionDetails) {
const message = evalResult.exceptionDetails.exception?.description || 'Unknown script error'
logger.warn('Runtime.evaluate raised exception', { message })
throw new Error(message)
}
const value = evalResult?.result?.value ?? evalResult?.result?.description ?? null
return value
}
public async reset(sessionId?: string) {
if (sessionId) {
const entry = this.windows.get(sessionId)
if (entry) {
this.closeWindow(entry.win, sessionId)
}
this.windows.delete(sessionId)
logger.info('Browser CDP context reset', { sessionId })
return
}
for (const [id, entry] of this.windows.entries()) {
this.closeWindow(entry.win, id)
this.windows.delete(id)
}
logger.info('Browser CDP context reset (all sessions)')
}
/**
* Fetches a URL and returns content in the specified format.
* @param url - The URL to fetch
* @param format - Output format: 'html', 'txt', 'markdown', or 'json' (default: 'markdown')
* @param timeout - Navigation timeout in milliseconds (default: 10000)
* @param sessionId - Session identifier (default: 'default')
* @returns Content in the requested format. For 'json', returns parsed object or { data: rawContent } if parsing fails
*/
public async fetch(
url: string,
format: 'html' | 'txt' | 'markdown' | 'json' = 'markdown',
timeout = 10000,
sessionId = 'default'
) {
await this.open(url, timeout, false, sessionId)
const win = await this.getWindow(sessionId)
const dbg = win.webContents.debugger
await this.ensureDebuggerAttached(dbg, sessionId)
let expression: string
if (format === 'json' || format === 'txt') {
expression = 'document.body.innerText'
} else {
expression = 'document.documentElement.outerHTML'
}
const result = (await dbg.sendCommand('Runtime.evaluate', {
expression,
returnByValue: true
})) as { result?: { value?: string } }
const content = result?.result?.value ?? ''
if (format === 'markdown') {
const turndownService = new TurndownService()
return turndownService.turndown(content)
}
if (format === 'json') {
// Attempt to parse as JSON; if content is not valid JSON, wrap it in a data object
try {
return JSON.parse(content)
} catch {
return { data: content }
}
}
return content
}
}

View File

@ -0,0 +1,3 @@
export { CdpBrowserController } from './controller'
export { BrowserServer } from './server'
export { BrowserServer as default } from './server'

View File

@ -0,0 +1,50 @@
import type { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { Server as MCServer } from '@modelcontextprotocol/sdk/server/index.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { app } from 'electron'
import { CdpBrowserController } from './controller'
import { toolDefinitions, toolHandlers } from './tools'
export class BrowserServer {
public server: Server
private controller = new CdpBrowserController()
constructor() {
const server = new MCServer(
{
name: '@cherry/browser',
version: '0.1.0'
},
{
capabilities: {
resources: {},
tools: {}
}
}
)
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: toolDefinitions
}
})
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
const handler = toolHandlers[name]
if (!handler) {
throw new Error('Tool not found')
}
return handler(this.controller, args)
})
app.on('before-quit', () => {
void this.controller.reset()
})
this.server = server
}
}
export default BrowserServer

View File

@ -0,0 +1,48 @@
import * as z from 'zod'
import type { CdpBrowserController } from '../controller'
import { errorResponse, successResponse } from './utils'
export const ExecuteSchema = z.object({
code: z
.string()
.describe(
'JavaScript evaluated via Chrome DevTools Runtime.evaluate. Keep it short; prefer one-line with semicolons for multiple statements.'
),
timeout: z.number().default(5000).describe('Timeout in milliseconds for code execution (default: 5000ms)'),
sessionId: z.string().optional().describe('Session identifier to target a specific page (default: default)')
})
export const executeToolDefinition = {
name: 'execute',
description:
'Run JavaScript in the current page via Runtime.evaluate. Prefer short, single-line snippets; use semicolons for multiple statements.',
inputSchema: {
type: 'object',
properties: {
code: {
type: 'string',
description: 'One-line JS to evaluate in page context'
},
timeout: {
type: 'number',
description: 'Timeout in milliseconds (default 5000)'
},
sessionId: {
type: 'string',
description: 'Session identifier; targets a specific page (default: default)'
}
},
required: ['code']
}
}
export async function handleExecute(controller: CdpBrowserController, args: unknown) {
const { code, timeout, sessionId } = ExecuteSchema.parse(args)
try {
const value = await controller.execute(code, timeout, sessionId ?? 'default')
return successResponse(typeof value === 'string' ? value : JSON.stringify(value))
} catch (error) {
return errorResponse(error as Error)
}
}

View File

@ -0,0 +1,49 @@
import * as z from 'zod'
import type { CdpBrowserController } from '../controller'
import { errorResponse, successResponse } from './utils'
export const FetchSchema = z.object({
url: z.url().describe('URL to fetch'),
format: z.enum(['html', 'txt', 'markdown', 'json']).default('markdown').describe('Output format (default: markdown)'),
timeout: z.number().optional().describe('Timeout in milliseconds for navigation (default: 10000)'),
sessionId: z.string().optional().describe('Session identifier (default: default)')
})
export const fetchToolDefinition = {
name: 'fetch',
description: 'Fetch a URL using the browser and return content in specified format (html, txt, markdown, json)',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL to fetch'
},
format: {
type: 'string',
enum: ['html', 'txt', 'markdown', 'json'],
description: 'Output format (default: markdown)'
},
timeout: {
type: 'number',
description: 'Navigation timeout in milliseconds (default: 10000)'
},
sessionId: {
type: 'string',
description: 'Session identifier (default: default)'
}
},
required: ['url']
}
}
export async function handleFetch(controller: CdpBrowserController, args: unknown) {
const { url, format, timeout, sessionId } = FetchSchema.parse(args)
try {
const content = await controller.fetch(url, format, timeout ?? 10000, sessionId ?? 'default')
return successResponse(typeof content === 'string' ? content : JSON.stringify(content))
} catch (error) {
return errorResponse(error as Error)
}
}

View File

@ -0,0 +1,25 @@
export { ExecuteSchema, executeToolDefinition, handleExecute } from './execute'
export { FetchSchema, fetchToolDefinition, handleFetch } from './fetch'
export { handleOpen, OpenSchema, openToolDefinition } from './open'
export { handleReset, resetToolDefinition } from './reset'
import type { CdpBrowserController } from '../controller'
import { executeToolDefinition, handleExecute } from './execute'
import { fetchToolDefinition, handleFetch } from './fetch'
import { handleOpen, openToolDefinition } from './open'
import { handleReset, resetToolDefinition } from './reset'
export const toolDefinitions = [openToolDefinition, executeToolDefinition, resetToolDefinition, fetchToolDefinition]
export const toolHandlers: Record<
string,
(
controller: CdpBrowserController,
args: unknown
) => Promise<{ content: { type: string; text: string }[]; isError: boolean }>
> = {
open: handleOpen,
execute: handleExecute,
reset: handleReset,
fetch: handleFetch
}

View File

@ -0,0 +1,47 @@
import * as z from 'zod'
import type { CdpBrowserController } from '../controller'
import { successResponse } from './utils'
export const OpenSchema = z.object({
url: z.url().describe('URL to open in the controlled Electron window'),
timeout: z.number().optional().describe('Timeout in milliseconds for navigation (default: 10000)'),
show: z.boolean().optional().describe('Whether to show the browser window (default: false)'),
sessionId: z
.string()
.optional()
.describe('Session identifier; separate sessions keep separate pages (default: default)')
})
export const openToolDefinition = {
name: 'open',
description: 'Open a URL in a hidden Electron window controlled via Chrome DevTools Protocol',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL to load'
},
timeout: {
type: 'number',
description: 'Navigation timeout in milliseconds (default 10000)'
},
show: {
type: 'boolean',
description: 'Whether to show the browser window (default false)'
},
sessionId: {
type: 'string',
description: 'Session identifier; separate sessions keep separate pages (default: default)'
}
},
required: ['url']
}
}
export async function handleOpen(controller: CdpBrowserController, args: unknown) {
const { url, timeout, show, sessionId } = OpenSchema.parse(args)
const res = await controller.open(url, timeout ?? 10000, show ?? false, sessionId ?? 'default')
return successResponse(JSON.stringify(res))
}

View File

@ -0,0 +1,34 @@
import * as z from 'zod'
import type { CdpBrowserController } from '../controller'
import { successResponse } from './utils'
/** Zod schema for validating reset tool arguments */
export const ResetSchema = z.object({
sessionId: z.string().optional().describe('Session identifier to reset; omit to reset all sessions')
})
/** MCP tool definition for the reset tool */
export const resetToolDefinition = {
name: 'reset',
description: 'Reset the controlled window and detach debugger',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Session identifier to reset; omit to reset all sessions'
}
}
}
}
/**
* Handler for the reset MCP tool.
* Closes browser window(s) and detaches debugger for the specified session or all sessions.
*/
export async function handleReset(controller: CdpBrowserController, args: unknown) {
const { sessionId } = ResetSchema.parse(args)
await controller.reset(sessionId)
return successResponse('reset')
}

View File

@ -0,0 +1,13 @@
export function successResponse(text: string) {
return {
content: [{ type: 'text', text }],
isError: false
}
}
export function errorResponse(error: Error) {
return {
content: [{ type: 'text', text: error.message }],
isError: true
}
}

View File

@ -0,0 +1,4 @@
import { loggerService } from '@logger'
export const logger = loggerService.withContext('MCPBrowserCDP')
export const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:145.0) Gecko/20100101 Firefox/145.0'

View File

@ -4,6 +4,7 @@ import type { BuiltinMCPServerName } from '@types'
import { BuiltinMCPServerNames } from '@types'
import BraveSearchServer from './brave-search'
import BrowserServer from './browser'
import DiDiMcpServer from './didi-mcp'
import DifyKnowledgeServer from './dify-knowledge'
import FetchServer from './fetch'
@ -48,6 +49,9 @@ export function createInMemoryMCPServer(
const apiKey = envs.DIDI_API_KEY
return new DiDiMcpServer(apiKey).server
}
case BuiltinMCPServerNames.browser: {
return new BrowserServer().server
}
default:
throw new Error(`Unknown in-memory MCP server: ${name}`)
}

View File

@ -343,7 +343,8 @@ const builtInMcpDescriptionKeyMap: Record<BuiltinMCPServerName, string> = {
[BuiltinMCPServerNames.filesystem]: 'settings.mcp.builtinServersDescriptions.filesystem',
[BuiltinMCPServerNames.difyKnowledge]: 'settings.mcp.builtinServersDescriptions.dify_knowledge',
[BuiltinMCPServerNames.python]: 'settings.mcp.builtinServersDescriptions.python',
[BuiltinMCPServerNames.didiMCP]: 'settings.mcp.builtinServersDescriptions.didi_mcp'
[BuiltinMCPServerNames.didiMCP]: 'settings.mcp.builtinServersDescriptions.didi_mcp',
[BuiltinMCPServerNames.browser]: 'settings.mcp.builtinServersDescriptions.browser'
} as const
export const getBuiltInMcpServerDescriptionLabel = (key: string): string => {

View File

@ -3884,6 +3884,7 @@
"builtinServers": "Builtin Servers",
"builtinServersDescriptions": {
"brave_search": "An MCP server implementation integrating the Brave Search API, providing both web and local search functionalities. Requires configuring the BRAVE_API_KEY environment variable",
"browser": "Control a headless Electron window via Chrome DevTools Protocol. Tools: open URL, execute single-line JS, reset session.",
"didi_mcp": "DiDi MCP server providing ride-hailing services including map search, price estimation, order management, and driver tracking. Only available in Mainland China. Requires configuring the DIDI_API_KEY environment variable",
"dify_knowledge": "Dify's MCP server implementation provides a simple API to interact with Dify. Requires configuring the Dify Key",
"fetch": "MCP server for retrieving URL web content",

View File

@ -3884,6 +3884,7 @@
"builtinServers": "内置服务器",
"builtinServersDescriptions": {
"brave_search": "一个集成了Brave 搜索 API 的 MCP 服务器实现,提供网页与本地搜索双重功能。需要配置 BRAVE_API_KEY 环境变量",
"browser": "通过 Chrome DevTools 协议控制隐藏的 Electron 窗口,支持打开 URL、执行单行 JS、重置会话",
"didi_mcp": "一个集成了滴滴 MCP 服务器实现,提供网约车服务包括地图搜索、价格预估、订单管理和司机跟踪。仅支持中国大陆地区。需要配置 DIDI_API_KEY 环境变量",
"dify_knowledge": "Dify 的 MCP 服务器实现,提供了一个简单的 API 来与 Dify 进行交互。需要配置 Dify Key",
"fetch": "用于获取 URL 网页内容的 MCP 服务器",

View File

@ -3884,6 +3884,7 @@
"builtinServers": "內建伺服器",
"builtinServersDescriptions": {
"brave_search": "一個整合了 Brave 搜尋 API 的 MCP 伺服器實做,提供網頁與本機搜尋雙重功能。需要設定 BRAVE_API_KEY 環境變數",
"browser": "透過 Chrome DevTools Protocol 控制 headless Electron 視窗。工具:開啟 URL、執行單行 JS、重設工作階段。",
"didi_mcp": "一個整合了滴滴 MCP 伺服器實做,提供網約車服務包括地圖搜尋、價格預估、訂單管理和司機追蹤。僅支援中國大陸地區。需要設定 DIDI_API_KEY 環境變數",
"dify_knowledge": "Dify 的 MCP 伺服器實做,提供了一個簡單的 API 來與 Dify 進行互動。需要設定 Dify Key",
"fetch": "用於取得 URL 網頁內容的 MCP 伺服器",

View File

@ -3884,6 +3884,7 @@
"builtinServers": "Integrierter Server",
"builtinServersDescriptions": {
"brave_search": "MCP-Server-Implementierung mit Brave-Search-API, die sowohl Web- als auch lokale Suchfunktionen bietet. BRAVE_API_KEY-Umgebungsvariable muss konfiguriert werden",
"browser": "Steuert ein headless Electron-Fenster über das Chrome DevTools Protocol. Tools: URL öffnen, einzeiligen JS ausführen, Sitzung zurücksetzen.",
"didi_mcp": "An integrated Didi MCP server implementation that provides ride-hailing services including map search, price estimation, order management, and driver tracking. Only available in mainland China. Requires the DIDI_API_KEY environment variable to be configured.",
"dify_knowledge": "MCP-Server-Implementierung von Dify, die einen einfachen API-Zugriff auf Dify bietet. Dify Key muss konfiguriert werden",
"fetch": "MCP-Server zum Abrufen von Webseiteninhalten",

View File

@ -31,25 +31,25 @@
}
},
"gitBash": {
"autoDetected": "[to be translated]:Using auto-detected Git Bash",
"autoDetected": "Χρησιμοποιείται αυτόματα εντοπισμένο Git Bash",
"clear": {
"button": "[to be translated]:Clear custom path"
"button": "Διαγραφή προσαρμοσμένης διαδρομής"
},
"customPath": "[to be translated]:Using custom path: {{path}}",
"customPath": "Χρησιμοποιείται προσαρμοσμένη διαδρομή: {{path}}",
"error": {
"description": "Το Git Bash απαιτείται για την εκτέλεση πρακτόρων στα Windows. Ο πράκτορας δεν μπορεί να λειτουργήσει χωρίς αυτό. Παρακαλούμε εγκαταστήστε το Git για Windows από",
"recheck": "Επανέλεγχος Εγκατάστασης του Git Bash",
"title": "Απαιτείται Git Bash"
},
"found": {
"title": "[to be translated]:Git Bash configured"
"title": "Το Git Bash διαμορφώθηκε"
},
"notFound": "Το Git Bash δεν βρέθηκε. Παρακαλώ εγκαταστήστε το πρώτα.",
"pick": {
"button": "[to be translated]:Select Git Bash Path",
"failed": "[to be translated]:Failed to set Git Bash path",
"invalidPath": "[to be translated]:Selected file is not a valid Git Bash executable (bash.exe).",
"title": "[to be translated]:Select Git Bash executable"
"button": "Επιλογή διαδρομής Git Bash",
"failed": "Αποτυχία ορισμού διαδρομής Git Bash",
"invalidPath": "Το επιλεγμένο αρχείο δεν είναι έγκυρο εκτελέσιμο Git Bash (bash.exe).",
"title": "Επιλογή εκτελέσιμου Git Bash"
},
"success": "Το Git Bash εντοπίστηκε με επιτυχία!"
},
@ -547,7 +547,7 @@
"medium": "Μεσαίο",
"minimal": "ελάχιστος",
"off": "Απενεργοποίηση",
"xhigh": "[to be translated]:Extra High"
"xhigh": "Εξαιρετικά Υψηλή"
},
"regular_phrases": {
"add": "Προσθήκη φράσης",
@ -3884,6 +3884,7 @@
"builtinServers": "Ενσωματωμένοι Διακομιστές",
"builtinServersDescriptions": {
"brave_search": "μια εφαρμογή διακομιστή MCP που ενσωματώνει το Brave Search API, παρέχοντας δυνατότητες αναζήτησης στον ιστό και τοπικής αναζήτησης. Απαιτείται η ρύθμιση της μεταβλητής περιβάλλοντος BRAVE_API_KEY",
"browser": "Ελέγχει ένα headless παράθυρο Electron μέσω του Chrome DevTools Protocol. Εργαλεία: άνοιγμα URL, εκτέλεση JS μίας γραμμής, επαναφορά συνεδρίας.",
"didi_mcp": "Διακομιστής DiDi MCP που παρέχει υπηρεσίες μεταφοράς συμπεριλαμβανομένης της αναζήτησης χαρτών, εκτίμησης τιμών, διαχείρισης παραγγελιών και παρακολούθησης οδηγών. Διαθέσιμο μόνο στην ηπειρωτική Κίνα. Απαιτεί διαμόρφωση της μεταβλητής περιβάλλοντος DIDI_API_KEY",
"dify_knowledge": "Η υλοποίηση του Dify για τον διακομιστή MCP, παρέχει μια απλή API για να αλληλεπιδρά με το Dify. Απαιτείται η ρύθμιση του κλειδιού Dify",
"fetch": "Εξυπηρετητής MCP για λήψη περιεχομένου ιστοσελίδας URL",

View File

@ -547,7 +547,7 @@
"medium": "Medio",
"minimal": "minimal",
"off": "Apagado",
"xhigh": "[to be translated]:Extra High"
"xhigh": "Extra Alta"
},
"regular_phrases": {
"add": "Agregar frase",
@ -3884,6 +3884,7 @@
"builtinServers": "Servidores integrados",
"builtinServersDescriptions": {
"brave_search": "Una implementación de servidor MCP que integra la API de búsqueda de Brave, proporcionando funciones de búsqueda web y búsqueda local. Requiere configurar la variable de entorno BRAVE_API_KEY",
"browser": "Controla una ventana Electron headless mediante Chrome DevTools Protocol. Herramientas: abrir URL, ejecutar JS de una línea, reiniciar sesión.",
"didi_mcp": "Servidor DiDi MCP que proporciona servicios de transporte incluyendo búsqueda de mapas, estimación de precios, gestión de pedidos y seguimiento de conductores. Disponible solo en China Continental. Requiere configurar la variable de entorno DIDI_API_KEY",
"dify_knowledge": "Implementación del servidor MCP de Dify, que proporciona una API sencilla para interactuar con Dify. Se requiere configurar la clave de Dify.",
"fetch": "Servidor MCP para obtener el contenido de la página web de una URL",

View File

@ -547,7 +547,7 @@
"medium": "Moyen",
"minimal": "minimal",
"off": "Off",
"xhigh": "[to be translated]:Extra High"
"xhigh": "Très élevée"
},
"regular_phrases": {
"add": "Добавить фразу",
@ -3884,6 +3884,7 @@
"builtinServers": "Serveurs intégrés",
"builtinServersDescriptions": {
"brave_search": "Une implémentation de serveur MCP intégrant l'API de recherche Brave, offrant des fonctionnalités de recherche web et locale. Nécessite la configuration de la variable d'environnement BRAVE_API_KEY",
"browser": "Contrôle une fenêtre Electron headless via Chrome DevTools Protocol. Outils : ouvrir une URL, exécuter du JS en une ligne, réinitialiser la session.",
"didi_mcp": "Serveur DiDi MCP fournissant des services de transport incluant la recherche de cartes, l'estimation des prix, la gestion des commandes et le suivi des conducteurs. Disponible uniquement en Chine continentale. Nécessite la configuration de la variable d'environnement DIDI_API_KEY",
"dify_knowledge": "Implémentation du serveur MCP de Dify, fournissant une API simple pour interagir avec Dify. Nécessite la configuration de la clé Dify",
"fetch": "serveur MCP utilisé pour récupérer le contenu des pages web URL",

View File

@ -31,25 +31,25 @@
}
},
"gitBash": {
"autoDetected": "[to be translated]:Using auto-detected Git Bash",
"autoDetected": "自動検出されたGit Bashを使用中",
"clear": {
"button": "[to be translated]:Clear custom path"
"button": "カスタムパスをクリア"
},
"customPath": "[to be translated]:Using custom path: {{path}}",
"customPath": "カスタムパスを使用中: {{path}}",
"error": {
"description": "Windowsでエージェントを実行するにはGit Bashが必要です。これがないとエージェントは動作しません。以下からGit for Windowsをインストールしてください。",
"recheck": "Git Bashのインストールを再確認してください",
"title": "Git Bashが必要です"
},
"found": {
"title": "[to be translated]:Git Bash configured"
"title": "Git Bashが設定されました"
},
"notFound": "Git Bash が見つかりません。先にインストールしてください。",
"pick": {
"button": "[to be translated]:Select Git Bash Path",
"failed": "[to be translated]:Failed to set Git Bash path",
"invalidPath": "[to be translated]:Selected file is not a valid Git Bash executable (bash.exe).",
"title": "[to be translated]:Select Git Bash executable"
"button": "Git Bashパスを選択",
"failed": "Git Bashパスの設定に失敗しました",
"invalidPath": "選択されたファイルは有効なGit Bash実行ファイルbash.exeではありません。",
"title": "Git Bash実行ファイルを選択"
},
"success": "Git Bashが正常に検出されました"
},
@ -547,7 +547,7 @@
"medium": "普通の思考",
"minimal": "最小限の思考",
"off": "オフ",
"xhigh": "[to be translated]:Extra High"
"xhigh": "超高"
},
"regular_phrases": {
"add": "プロンプトを追加",
@ -3884,6 +3884,7 @@
"builtinServers": "組み込みサーバー",
"builtinServersDescriptions": {
"brave_search": "Brave検索APIを統合したMCPサーバーの実装で、ウェブ検索とローカル検索の両機能を提供します。BRAVE_API_KEY環境変数の設定が必要です",
"browser": "Chrome DevTools Protocolを介してheadless Electronウィンドウを制御します。ツールURLを開く、単一行JSを実行、セッションをリセット。",
"didi_mcp": "DiDi MCPサーバーは、地図検索、料金見積もり、注文管理、ドライバー追跡を含むライドシェアサービスを提供します。中国本土でのみ利用可能です。DIDI_API_KEY環境変数の設定が必要です",
"dify_knowledge": "DifyのMCPサーバー実装は、Difyと対話するためのシンプルなAPIを提供します。Dify Keyの設定が必要です。",
"fetch": "URLのウェブページコンテンツを取得するためのMCPサーバー",

View File

@ -547,7 +547,7 @@
"medium": "Médio",
"minimal": "mínimo",
"off": "Desligado",
"xhigh": "[to be translated]:Extra High"
"xhigh": "Extra Alta"
},
"regular_phrases": {
"add": "Adicionar Frase",
@ -3884,6 +3884,7 @@
"builtinServers": "Servidores integrados",
"builtinServersDescriptions": {
"brave_search": "uma implementação de servidor MCP integrada com a API de pesquisa Brave, fornecendo funcionalidades de pesquisa web e local. Requer a configuração da variável de ambiente BRAVE_API_KEY",
"browser": "Controla uma janela Electron headless via Chrome DevTools Protocol. Ferramentas: abrir URL, executar JS de linha única, reiniciar sessão.",
"didi_mcp": "Servidor DiDi MCP que fornece serviços de transporte incluindo pesquisa de mapas, estimativa de preços, gestão de pedidos e rastreamento de motoristas. Disponível apenas na China Continental. Requer configuração da variável de ambiente DIDI_API_KEY",
"dify_knowledge": "Implementação do servidor MCP do Dify, que fornece uma API simples para interagir com o Dify. Requer a configuração da chave Dify",
"fetch": "servidor MCP para obter o conteúdo da página web do URL",

View File

@ -547,7 +547,7 @@
"medium": "Среднее",
"minimal": "минимальный",
"off": "Выключить",
"xhigh": "[to be translated]:Extra High"
"xhigh": "Сверхвысокое"
},
"regular_phrases": {
"add": "Добавить подсказку",
@ -3884,6 +3884,7 @@
"builtinServers": "Встроенные серверы",
"builtinServersDescriptions": {
"brave_search": "реализация сервера MCP с интеграцией API поиска Brave, обеспечивающая функции веб-поиска и локального поиска. Требуется настройка переменной среды BRAVE_API_KEY",
"browser": "Управление headless-окном Electron через Chrome DevTools Protocol. Инструменты: открытие URL, выполнение однострочного JS, сброс сессии.",
"didi_mcp": "Сервер DiDi MCP, предоставляющий услуги такси, включая поиск на карте, оценку стоимости, управление заказами и отслеживание водителей. Доступен только в материковом Китае. Требует настройки переменной окружения DIDI_API_KEY",
"dify_knowledge": "Реализация сервера MCP Dify, предоставляющая простой API для взаимодействия с Dify. Требуется настройка ключа Dify",
"fetch": "MCP-сервер для получения содержимого веб-страниц по URL",

View File

@ -174,6 +174,15 @@ export const builtinMCPServers: BuiltinMCPServer[] = [
provider: 'CherryAI',
installSource: 'builtin',
isTrusted: true
},
{
id: nanoid(),
name: BuiltinMCPServerNames.browser,
type: 'inMemory',
isActive: false,
provider: 'CherryAI',
installSource: 'builtin',
isTrusted: true
}
] as const

View File

@ -749,7 +749,8 @@ export const BuiltinMCPServerNames = {
filesystem: '@cherry/filesystem',
difyKnowledge: '@cherry/dify-knowledge',
python: '@cherry/python',
didiMCP: '@cherry/didi-mcp'
didiMCP: '@cherry/didi-mcp',
browser: '@cherry/browser'
} as const
export type BuiltinMCPServerName = (typeof BuiltinMCPServerNames)[keyof typeof BuiltinMCPServerNames]