Skip to main content
Glama
visionTools.ts16.3 kB
/** * @file src/tools/visionTools.ts * @description Outils de vision pour Browser-Manager-MCP-Server-dist * Capacité Vision pour le contrôle avancé de la souris et reconnaissance visuelle */ import { z } from 'zod'; import type { Context } from 'fastmcp'; import type { AuthData } from '../types.js'; import { ToolCategories, ToolAnnotations } from './categories.js'; import { Capability } from '../capabilities/index.js'; // Import des états globaux depuis browserTools import { pages, currentPageId } from './browserTools.js'; // Tool: mouse_move_xy export const mouseMoveXyTool = { name: 'mouse_move_xy', description: 'Déplace le curseur de la souris à des coordonnées absolues sur la page', annotations: { category: ToolCategories.INTERACTION, readOnlyHint: false, requiresAuth: false, dangerous: false, experimental: false, } as ToolAnnotations, parameters: z.object({ x: z.number().describe('Coordonnée X absolue sur la page'), y: z.number().describe('Coordonnée Y absolue sur la page'), pageId: z.string().optional().describe('ID de la page, par défaut la page courante'), steps: z .number() .optional() .default(1) .describe("Nombre d'étapes pour le mouvement (pour mouvement fluide)"), duration: z.number().optional().default(100).describe('Durée du mouvement en millisecondes'), }), execute: async (args: any, _context: Context<AuthData>) => { const { x, y, pageId = currentPageId, steps, duration } = args; if (!pageId || !pages.has(pageId)) { throw new Error('Aucune page active disponible pour le mouvement de souris'); } const page = pages.get(pageId)!; try { // Validation des coordonnées const viewport = page.viewportSize(); if (viewport) { if (x < 0 || x > viewport.width || y < 0 || y > viewport.height) { throw new Error( `Coordonnées hors limites. Page: ${viewport.width}x${viewport.height}, Reçu: ${x},${y}` ); } } // Mouvement simple await page.mouse.move(x, y); return `Souris déplacée vers les coordonnées (${x}, ${y}) avec ${steps} étape(s) sur ${duration}ms`; } catch (error: any) { throw new Error(`Erreur lors du mouvement de souris: ${error.message}`); } }, }; // Tool: mouse_click_xy export const mouseClickXyTool = { name: 'mouse_click_xy', description: 'Clique à des coordonnées absolues sur la page', annotations: { category: ToolCategories.INTERACTION, readOnlyHint: false, requiresAuth: false, dangerous: false, experimental: false, } as ToolAnnotations, parameters: z.object({ x: z.number().describe('Coordonnée X absolue sur la page'), y: z.number().describe('Coordonnée Y absolue sur la page'), pageId: z.string().optional().describe('ID de la page, par défaut la page courante'), button: z .enum(['left', 'middle', 'right']) .optional() .default('left') .describe('Bouton de la souris à utiliser'), clickCount: z .number() .optional() .default(1) .describe('Nombre de clics (1 pour simple, 2 pour double-clic)'), delay: z.number().optional().default(50).describe('Délai entre les clics en millisecondes'), moveBeforeClick: z .boolean() .optional() .default(true) .describe('Déplacer la souris avant de cliquer'), }), execute: async (args: any, _context: Context<AuthData>) => { const { x, y, pageId = currentPageId, button, clickCount, delay, moveBeforeClick } = args; if (!pageId || !pages.has(pageId)) { throw new Error('Aucune page active disponible pour le clic de souris'); } const page = pages.get(pageId)!; try { // Validation des coordonnées const viewport = page.viewportSize(); if (viewport) { if (x < 0 || x > viewport.width || y < 0 || y > viewport.height) { throw new Error( `Coordonnées hors limites. Page: ${viewport.width}x${viewport.height}, Reçu: ${x},${y}` ); } } // Déplacer la souris avant de cliquer si demandé if (moveBeforeClick) { await page.mouse.move(x, y); await page.waitForTimeout(50); } // Effectuer les clics const clickOptions = { button: button as 'left' | 'middle' | 'right', clickCount, delay, }; await page.mouse.click(x, y, clickOptions); const clickType = clickCount === 1 ? 'clic' : 'double-clic'; const buttonText = button === 'left' ? 'gauche' : button === 'right' ? 'droit' : 'milieu'; return `${clickType} ${buttonText} effectué aux coordonnées (${x}, ${y})`; } catch (error: any) { throw new Error(`Erreur lors du clic de souris: ${error.message}`); } }, }; // Tool: mouse_drag_xy export const mouseDragXyTool = { name: 'mouse_drag_xy', description: 'Effectue un glisser-déposer (drag and drop) entre deux coordonnées', annotations: { category: ToolCategories.INTERACTION, readOnlyHint: false, requiresAuth: false, dangerous: false, experimental: false, } as ToolAnnotations, parameters: z.object({ fromX: z.number().describe('Coordonnée X de départ'), fromY: z.number().describe('Coordonnée Y de départ'), toX: z.number().describe("Coordonnée X d'arrivée"), toY: z.number().describe("Coordonnée Y d'arrivée"), pageId: z.string().optional().describe('ID de la page, par défaut la page courante'), button: z .enum(['left', 'middle', 'right']) .optional() .default('left') .describe('Bouton de la souris à utiliser'), steps: z .number() .optional() .default(10) .describe("Nombre d'étapes pour le mouvement de glissement"), duration: z.number().optional().default(500).describe('Durée du glissement en millisecondes'), delayBeforeDrop: z .number() .optional() .default(100) .describe('Délai avant de relâcher le bouton en millisecondes'), }), execute: async (args: any, _context: Context<AuthData>) => { const { fromX, fromY, toX, toY, pageId = currentPageId, button, steps, duration, delayBeforeDrop, } = args; if (!pageId || !pages.has(pageId)) { throw new Error('Aucune page active disponible pour le glisser-déposer'); } const page = pages.get(pageId)!; try { // Validation des coordonnées const viewport = page.viewportSize(); if (viewport) { const allCoords = [fromX, fromY, toX, toY]; for (const coord of allCoords) { if (coord < 0 || coord > Math.max(viewport.width, viewport.height)) { throw new Error(`Coordonnées hors limites. Page: ${viewport.width}x${viewport.height}`); } } } // Mouvement vers le point de départ await page.mouse.move(fromX, fromY); await page.waitForTimeout(50); // Presser le bouton de la souris await page.mouse.down({ button: button as 'left' | 'middle' | 'right' }); // Glissement progressif const stepDuration = duration / steps; const deltaX = (toX - fromX) / steps; const deltaY = (toY - fromY) / steps; for (let i = 1; i <= steps; i++) { const currentX = fromX + deltaX * i; const currentY = fromY + deltaY * i; await page.mouse.move(currentX, currentY); await page.waitForTimeout(stepDuration); } // Délai avant de relâcher if (delayBeforeDrop > 0) { await page.waitForTimeout(delayBeforeDrop); } // Relâcher le bouton await page.mouse.up({ button: button as 'left' | 'middle' | 'right' }); const buttonText = button === 'left' ? 'gauche' : button === 'right' ? 'droit' : 'milieu'; return `Glisser-déposer effectué avec bouton ${buttonText} de (${fromX}, ${fromY}) vers (${toX}, ${toY}) en ${steps} étapes`; } catch (error: any) { throw new Error(`Erreur lors du glisser-déposer: ${error.message}`); } }, }; // Tool: visual_locate export const visualLocateTool = { name: 'visual_locate', description: 'Localise visuellement des éléments sur la page et retourne leurs coordonnées', annotations: { category: ToolCategories.INTERACTION, readOnlyHint: true, requiresAuth: false, dangerous: false, experimental: true, } as ToolAnnotations, parameters: z.object({ selector: z.string().optional().describe("Sélecteur CSS de l'élément à localiser"), text: z.string().optional().describe('Texte à rechercher sur la page'), pageId: z.string().optional().describe('ID de la page, par défaut la page courante'), tolerance: z .number() .optional() .default(5) .describe('Tolérance pour la recherche visuelle en pixels'), multiple: z .boolean() .optional() .default(false) .describe('Retourner toutes les occurrences ou seulement la première'), }), execute: async (args: any, _context: Context<AuthData>) => { const { selector, text, pageId = currentPageId, multiple } = args; if (!pageId || !pages.has(pageId)) { throw new Error('Aucune page active disponible pour la localisation visuelle'); } if (!selector && !text) { throw new Error('Veuillez spécifier soit un sélecteur CSS soit un texte à rechercher'); } const page = pages.get(pageId)!; try { let elements: any[] = []; if (selector) { // Recherche par sélecteur CSS const foundElements = await page.$$(selector); for (let i = 0; i < foundElements.length; i++) { const element = foundElements[i]; const boundingBox = await element.boundingBox(); if (boundingBox) { elements.push({ index: i, selector, type: 'css_selector', boundingBox: { x: Math.round(boundingBox.x), y: Math.round(boundingBox.y), width: Math.round(boundingBox.width), height: Math.round(boundingBox.height), }, center: { x: Math.round(boundingBox.x + boundingBox.width / 2), y: Math.round(boundingBox.y + boundingBox.height / 2), }, }); } } } else if (text) { // Recherche par texte simplifiée elements = await page.evaluate((searchText: string) => { const results: any[] = []; const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null); let node; while ((node = walker.nextNode())) { if (node.textContent && node.textContent.includes(searchText)) { const range = document.createRange(); range.selectNodeContents(node); const rects = range.getClientRects(); for (let i = 0; i < rects.length; i++) { const rect = rects[i]; if (rect.width > 0 && rect.height > 0) { results.push({ index: results.length, text: searchText, type: 'text_search', boundingBox: { x: Math.round(rect.left + window.scrollX), y: Math.round(rect.top + window.scrollY), width: Math.round(rect.width), height: Math.round(rect.height), }, center: { x: Math.round(rect.left + rect.width / 2 + window.scrollX), y: Math.round(rect.top + rect.height / 2 + window.scrollY), }, }); } } } } return results; }, text); } if (elements.length === 0) { return `Aucun élément trouvé pour la recherche: ${selector || text}`; } // Limiter les résultats si multiple est false const resultsToShow = multiple ? elements : [elements[0]]; const response = { query: selector || text, queryType: selector ? 'css_selector' : 'text_search', totalFound: elements.length, returned: resultsToShow.length, elements: resultsToShow, viewport: await page.viewportSize(), timestamp: new Date().toISOString(), }; return JSON.stringify(response, null, 2); } catch (error: any) { throw new Error(`Erreur lors de la localisation visuelle: ${error.message}`); } }, }; // Tool: get_viewport_size export const getViewportSizeTool = { name: 'get_viewport_size', description: 'Retourne les dimensions actuelles de la vue (viewport) de la page', annotations: { category: ToolCategories.INTERACTION, readOnlyHint: true, requiresAuth: false, dangerous: false, experimental: false, } as ToolAnnotations, parameters: z.object({ pageId: z.string().optional().describe('ID de la page, par défaut la page courante'), }), execute: async (args: any, _context: Context<AuthData>) => { const { pageId = currentPageId } = args; if (!pageId || !pages.has(pageId)) { throw new Error('Aucune page active disponible'); } const page = pages.get(pageId)!; try { const viewport = page.viewportSize(); const scrollPosition = await page.evaluate(() => ({ x: window.scrollX, y: window.scrollY, })); const pageDimensions = await page.evaluate(() => ({ width: document.documentElement.scrollWidth, height: document.documentElement.scrollHeight, })); const response = { viewport: { width: viewport?.width || 0, height: viewport?.height || 0, }, scrollPosition, pageDimensions, mousePosition: { x: 0, y: 0 }, timestamp: new Date().toISOString(), }; return JSON.stringify(response, null, 2); } catch (error: any) { throw new Error(`Erreur lors de l'obtention des dimensions: ${error.message}`); } }, }; // Tool: vision_info export const visionInfoTool = { name: 'vision_info', description: 'Affiche les informations sur les capacités de vision et les outils disponibles', annotations: { category: ToolCategories.INTERACTION, readOnlyHint: true, requiresAuth: false, dangerous: false, experimental: false, } as ToolAnnotations, parameters: z.object({ pageId: z.string().optional().describe('ID de la page pour vérifier la compatibilité'), }), execute: async (args: any, _context: Context<AuthData>) => { const { pageId = currentPageId } = args; const info = { capability: Capability.VISION, enabled: true, availableTools: [ 'mouse_move_xy', 'mouse_click_xy', 'mouse_drag_xy', 'visual_locate', 'get_viewport_size', 'vision_info', ], features: { mouseControl: 'Contrôle précis de la souris avec coordonnées absolues', visualLocation: "Localisation d'éléments par sélecteur CSS ou texte", dragDrop: 'Glisser-déposer avec contrôle du mouvement', viewportInfo: 'Informations sur les dimensions de la page', smoothMovement: 'Mouvements fluides avec étapes configurables', }, coordinateSystem: { origin: 'coin supérieur gauche (0,0)', units: 'pixels', viewportRelative: 'true', }, currentPage: pageId || 'Aucune', pageCount: pages.size, }; if (pageId && pages.has(pageId)) { const page = pages.get(pageId)!; try { const viewport = page.viewportSize(); info.currentPage = `${pageId} - Viewport: ${viewport?.width}x${viewport?.height}`; } catch { info.currentPage = `${pageId} (erreur lors de la récupération des infos)`; } } return JSON.stringify(info, null, 2); }, }; export const visionTools = [ mouseMoveXyTool, mouseClickXyTool, mouseDragXyTool, visualLocateTool, getViewportSizeTool, visionInfoTool, ];

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DeamonDev888/Browser-Manager-MCP-Server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server