UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

492 lines (491 loc) 13.6 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import puppeteer from "puppeteer"; import { logger } from "../../core/monitoring/logger.js"; class BrowserMCPIntegration { // 30 minutes constructor(config = {}) { this.config = config; this.startCleanupInterval(); } sessions = /* @__PURE__ */ new Map(); server; maxSessions = 5; sessionTimeout = 30 * 60 * 1e3; /** * Initialize the Browser MCP server */ async initialize(mcpServer) { this.server = mcpServer || new Server( { name: "stackmemory-browser", version: "1.0.0" }, { capabilities: { tools: {} } } ); this.setupHandlers(); logger.info("Browser MCP integration initialized"); } /** * Set up MCP request handlers */ setupHandlers() { if (!this.server) return; this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "browser_navigate", description: "Navigate to a URL in the browser", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL to navigate to" }, sessionId: { type: "string", description: "Optional session ID" } }, required: ["url"] } }, { name: "browser_screenshot", description: "Take a screenshot of the current page", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Session ID" }, fullPage: { type: "boolean", description: "Capture full page" }, selector: { type: "string", description: "CSS selector to screenshot" } }, required: ["sessionId"] } }, { name: "browser_click", description: "Click an element on the page", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Session ID" }, selector: { type: "string", description: "CSS selector to click" } }, required: ["sessionId", "selector"] } }, { name: "browser_type", description: "Type text into an input field", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Session ID" }, selector: { type: "string", description: "CSS selector of input" }, text: { type: "string", description: "Text to type" } }, required: ["sessionId", "selector", "text"] } }, { name: "browser_evaluate", description: "Execute JavaScript in the browser context", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Session ID" }, script: { type: "string", description: "JavaScript code to execute" } }, required: ["sessionId", "script"] } }, { name: "browser_wait", description: "Wait for an element or condition", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Session ID" }, selector: { type: "string", description: "CSS selector to wait for" }, timeout: { type: "number", description: "Timeout in milliseconds" } }, required: ["sessionId"] } }, { name: "browser_get_content", description: "Get the text content of the page or element", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Session ID" }, selector: { type: "string", description: "CSS selector (optional)" } }, required: ["sessionId"] } }, { name: "browser_close", description: "Close a browser session", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Session ID to close" } }, required: ["sessionId"] } } ] })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (!args) { throw new McpError(ErrorCode.InvalidParams, "Missing arguments"); } try { switch (name) { case "browser_navigate": return await this.navigate( String(args.url), args.sessionId ); case "browser_screenshot": return await this.screenshot( String(args.sessionId), args.fullPage, args.selector ); case "browser_click": return await this.click( String(args.sessionId), String(args.selector) ); case "browser_type": return await this.type( String(args.sessionId), String(args.selector), String(args.text) ); case "browser_evaluate": return await this.evaluate( String(args.sessionId), String(args.script) ); case "browser_wait": return await this.waitFor( String(args.sessionId), args.selector, args.timeout ); case "browser_get_content": return await this.getContent( String(args.sessionId), args.selector ); case "browser_close": return await this.closeSession(String(args.sessionId)); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { logger.error("Browser MCP tool error", error); throw new McpError(ErrorCode.InternalError, error.message); } }); } /** * Navigate to a URL */ async navigate(url, sessionId) { const session = await this.getOrCreateSession(sessionId); await session.page.goto(url, { waitUntil: "networkidle2" }); session.url = url; session.lastActivity = /* @__PURE__ */ new Date(); logger.info(`Browser navigated to ${url}`, { sessionId: session.id }); return { content: [ { type: "text", text: `Navigated to ${url}` } ], sessionId: session.id, url }; } /** * Take a screenshot */ async screenshot(sessionId, fullPage = false, selector) { const session = this.getSession(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } let screenshot; if (selector) { const element = await session.page.$(selector); if (!element) { throw new Error(`Element ${selector} not found`); } screenshot = Buffer.from(await element.screenshot()); } else { screenshot = Buffer.from(await session.page.screenshot({ fullPage })); } session.lastActivity = /* @__PURE__ */ new Date(); return { content: [ { type: "image", data: screenshot.toString("base64") } ], sessionId: session.id }; } /** * Click an element */ async click(sessionId, selector) { const session = this.getSession(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } await session.page.click(selector); session.lastActivity = /* @__PURE__ */ new Date(); return { content: [ { type: "text", text: `Clicked element: ${selector}` } ], sessionId: session.id }; } /** * Type text into an input */ async type(sessionId, selector, text) { const session = this.getSession(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } await session.page.type(selector, text); session.lastActivity = /* @__PURE__ */ new Date(); return { content: [ { type: "text", text: `Typed "${text}" into ${selector}` } ], sessionId: session.id }; } /** * Execute JavaScript in page context */ async evaluate(sessionId, script) { const session = this.getSession(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } const result = await session.page.evaluate(script); session.lastActivity = /* @__PURE__ */ new Date(); return { content: [ { type: "text", text: JSON.stringify(result, null, 2) } ], sessionId: session.id, result }; } /** * Wait for element or timeout */ async waitFor(sessionId, selector, timeout = 5e3) { const session = this.getSession(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } if (selector) { await session.page.waitForSelector(selector, { timeout }); } else { await new Promise((resolve) => setTimeout(resolve, timeout)); } session.lastActivity = /* @__PURE__ */ new Date(); return { content: [ { type: "text", text: selector ? `Element ${selector} found` : `Waited ${timeout}ms` } ], sessionId: session.id }; } /** * Get page content */ async getContent(sessionId, selector) { const session = this.getSession(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found`); } let content; if (selector) { content = await session.page.$eval( selector, (el) => el.textContent || "" ); } else { content = await session.page.content(); } session.lastActivity = /* @__PURE__ */ new Date(); return { content: [ { type: "text", text: content } ], sessionId: session.id }; } /** * Get or create a browser session */ async getOrCreateSession(sessionId) { if (sessionId) { const existing = this.sessions.get(sessionId); if (existing) { existing.lastActivity = /* @__PURE__ */ new Date(); return existing; } } if (this.sessions.size >= this.maxSessions) { const oldest = Array.from(this.sessions.values()).sort( (a, b) => a.lastActivity.getTime() - b.lastActivity.getTime() )[0]; await this.closeSession(oldest.id); } const browser = await puppeteer.launch({ headless: this.config.headless ?? true, defaultViewport: this.config.defaultViewport || { width: 1280, height: 720 }, userDataDir: this.config.userDataDir, executablePath: this.config.executablePath, args: ["--no-sandbox", "--disable-setuid-sandbox"] // For Railway/Docker }); const page = await browser.newPage(); const id = sessionId || `session-${Date.now()}`; const session = { id, browser, page, createdAt: /* @__PURE__ */ new Date(), lastActivity: /* @__PURE__ */ new Date() }; this.sessions.set(id, session); logger.info(`Created browser session ${id}`); return session; } /** * Get existing session */ getSession(sessionId) { return this.sessions.get(sessionId); } /** * Close a browser session */ async closeSession(sessionId) { const session = this.sessions.get(sessionId); if (!session) { return { content: [ { type: "text", text: `Session ${sessionId} not found` } ] }; } await session.browser.close(); this.sessions.delete(sessionId); logger.info(`Closed browser session ${sessionId}`); return { content: [ { type: "text", text: `Session ${sessionId} closed` } ] }; } /** * Clean up inactive sessions */ startCleanupInterval() { setInterval(async () => { const now = Date.now(); for (const [id, session] of this.sessions.entries()) { const inactiveTime = now - session.lastActivity.getTime(); if (inactiveTime > this.sessionTimeout) { logger.info(`Cleaning up inactive session ${id}`); await this.closeSession(id); } } }, 6e4); } /** * Close all sessions */ async cleanup() { for (const sessionId of this.sessions.keys()) { await this.closeSession(sessionId); } } } export { BrowserMCPIntegration };