mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-18 22:06:39 +08:00
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:
parent
aeebd343d7
commit
d41229c69b
@ -23,7 +23,7 @@
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": ["**", "!**/.claude/**", "!**/.vscode/**"],
|
||||
"includes": ["**", "!**/.claude/**", "!**/.vscode/**", "!**/.conductor/**"],
|
||||
"maxSize": 2097152
|
||||
},
|
||||
"formatter": {
|
||||
|
||||
@ -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',
|
||||
|
||||
134
src/main/mcpServers/__tests__/browser.test.ts
Normal file
134
src/main/mcpServers/__tests__/browser.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
307
src/main/mcpServers/browser/controller.ts
Normal file
307
src/main/mcpServers/browser/controller.ts
Normal 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
|
||||
}
|
||||
}
|
||||
3
src/main/mcpServers/browser/index.ts
Normal file
3
src/main/mcpServers/browser/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { CdpBrowserController } from './controller'
|
||||
export { BrowserServer } from './server'
|
||||
export { BrowserServer as default } from './server'
|
||||
50
src/main/mcpServers/browser/server.ts
Normal file
50
src/main/mcpServers/browser/server.ts
Normal 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
|
||||
48
src/main/mcpServers/browser/tools/execute.ts
Normal file
48
src/main/mcpServers/browser/tools/execute.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
49
src/main/mcpServers/browser/tools/fetch.ts
Normal file
49
src/main/mcpServers/browser/tools/fetch.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
25
src/main/mcpServers/browser/tools/index.ts
Normal file
25
src/main/mcpServers/browser/tools/index.ts
Normal 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
|
||||
}
|
||||
47
src/main/mcpServers/browser/tools/open.ts
Normal file
47
src/main/mcpServers/browser/tools/open.ts
Normal 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))
|
||||
}
|
||||
34
src/main/mcpServers/browser/tools/reset.ts
Normal file
34
src/main/mcpServers/browser/tools/reset.ts
Normal 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')
|
||||
}
|
||||
13
src/main/mcpServers/browser/tools/utils.ts
Normal file
13
src/main/mcpServers/browser/tools/utils.ts
Normal 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
|
||||
}
|
||||
}
|
||||
4
src/main/mcpServers/browser/types.ts
Normal file
4
src/main/mcpServers/browser/types.ts
Normal 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'
|
||||
@ -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}`)
|
||||
}
|
||||
|
||||
@ -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 => {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 服务器",
|
||||
|
||||
@ -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 伺服器",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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サーバー",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user