@stackmemoryai/stackmemory
Version:
Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.
493 lines (492 loc) • 13.7 kB
JavaScript
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
};
//# sourceMappingURL=browser-mcp.js.map