#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
CallToolResult,
TextContent,
ImageContent,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import puppeteer, { Browser, Page } from "puppeteer-core";
/** 消息状态是否开启 */
const NStaus = false;
// Environment variables configuration
const requiredEnvVars = {
SESSION_ID: process.env.SESSION_ID,
API_KEY: process.env.API_KEY,
};
// Validate required environment variables
Object.entries(requiredEnvVars).forEach(([name, value]) => {
//if (!value) throw new Error(`${name} environment variable is required`);
});
// 2. Global State
const browsers = new Map<string, { browser: Browser; page: Page }>();
const screenshots = new Map<string, string>();
// Global state variable for the default browser session
let defaultBrowserSession: { browser: Browser; page: Page } | null = null;
const sessionId = "default"; // Using a consistent session ID for the default session
// Ensure browser session is initialized and valid
async function ensureBrowserSession(): Promise<{
browser: Browser;
page: Page;
}> {
try {
// If no session exists, create one
if (!defaultBrowserSession) {
defaultBrowserSession = await createNewBrowserSession(
process.env.SESSION_ID!,
process.env.API_KEY!
);
return defaultBrowserSession;
}
// await defaultBrowserSession.page.evaluate(() => document.title);
return defaultBrowserSession;
} catch (error) {
throw error;
}
}
const sendNotification = (params: any) => {
if (!NStaus) return;
server.notification(params);
};
// 3. Helper Functions
async function getBrowserUrl(sessionId: string, apiKey: string) {
try {
// 发起请求
const response = await fetch(
`https://cloud.yunlogin.com/v2/cloudbrowser/api/session/start?apiKey=${apiKey}&sessionId=${
sessionId || ""
}`,
{
method: "POST", // 根据实际情况可能需要调整请求方法
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({}), // 如果需要传递数据,可以在这里添加
}
);
// 检查响应状态
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
server.notification({
method: "notifications/resources/list_changed",
});
// 解析响应体
const data = await response.json();
// 提取 browserUrl
if (data.code === 200 && data.data && data.data.browserUrl) {
return data.data.browserUrl;
} else {
throw new Error(
`Failed to get browserUrl. Response: ${JSON.stringify(data)}`
);
}
} catch (error) {
console.error("Error fetching browserUrl:", error);
throw error;
}
}
async function createNewBrowserSession(sessionId: string, apiKey: string) {
sendNotification({
method: "tool.connect",
params: { sessionId, apiKey },
});
// 通过 fetch 获取 browserUrl
const connectUrl = await getBrowserUrl(sessionId, apiKey);
const browser = await puppeteer.connect({
browserWSEndpoint: connectUrl,
});
const page = (await browser.pages())[0];
browsers.set(sessionId, { browser, page });
return { browser, page };
}
// 4. Tool Definitions
const TOOLS: Tool[] = [
{
name: "yunbrowser_navigate", // 导航到指定 URL
description: "Navigate to a URL",
inputSchema: {
type: "object",
properties: {
url: { type: "string" },
},
required: ["url"],
},
},
{
name: "yunbrowser_evaluate", // 执行 JavaScript 代码
description: "Evaluate JavaScript in the browser",
inputSchema: {
type: "object",
properties: {
script: { type: "string" },
},
required: ["script"],
},
},
{
name: "yunbrowser_get_current_url", // 获取当前 URL
description: "Retrieve the current URL of the browser page",
inputSchema: {
type: "object",
properties: {},
},
},
{
name: "yunbrowser_screenshot", // 截图
description: "Take a screenshot of the current page or a specific element",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name of the screenshot",
},
},
},
},
{
name: "yunbrowser_click", // 点击元素
description: "Click an element on the page",
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
description: "CSS selector for element to click",
},
},
required: ["selector"],
},
},
{
name: "yunbrowser_hover", // 点击元素
description: "Hover an element on the page",
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
description: "CSS selector for element to hover",
},
},
required: ["selector"],
},
},
{
name: "yunbrowser_fill", // 填充输入框
description: "Fill out an input field",
inputSchema: {
type: "object",
properties: {
selector: {
type: "string",
description: "CSS selector for input field",
},
value: { type: "string", description: "Value to fill" },
},
required: ["selector", "value"],
},
},
{
name: "yunbrowser_get_text", // 获取文本内容
description: "Extract all text content from the current page",
inputSchema: {
type: "object",
properties: {},
required: [],
},
},
];
// 5. Tool Handler Implementation
async function handleToolCall(
name: string,
args: any
): Promise<CallToolResult> {
try {
let session: { browser: Browser; page: Page } | undefined;
// For tools that don't need a session, skip session check
if (!["yunbrowser_create_session"].includes(name)) {
// Use or create the default session
session = await ensureBrowserSession();
}
//console.info(`Handling tool call: ${name}`, args);
switch (name) {
case "yunbrowser_navigate":
sendNotification({
method: "tool.goto",
params: {
msg: session!.page.isClosed(),
},
});
// 检查页面是否已关闭
if (session!.page.isClosed()) {
const pages = await session!.browser.pages();
/**
* 检查是否有页面
*
* 1. 如果有页面,使用第一个页面
* 2. 如果没有页面,创建一个新页面
*/
if (pages.length > 0) {
session!.page = pages[0];
} else {
session!.page = await session!.browser.newPage();
}
}
await session!.page.goto(args.url, {
timeout: 30000,
waitUntil: "load",
});
return {
content: [
{
type: "text",
text: `Navigated to ${args.url}`,
},
],
isError: false,
};
case "yunbrowser_evaluate":
try {
// 检查页面是否已关闭
if (session!.page.isClosed()) {
return {
content: [
{
type: "text",
text: `Page has been closed`,
},
],
isError: true,
};
}
const result = await session!.page.evaluate(args.script);
return {
content: [
{
type: "text",
text: `Evaluated script: ${JSON.stringify(result)}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to Evaluated script: ${args.script}: ${
(error as Error).message
}`,
},
],
isError: true,
};
}
case "yunbrowser_get_current_url":
// 检查页面是否已关闭
if (session!.page.isClosed()) {
return {
content: [
{
type: "text",
text: `Page has been closed`,
},
],
isError: true,
};
}
const currentUrl = await session!.page.url();
return {
content: [
{
type: "text",
text: `Current URL: ${currentUrl}`,
},
],
isError: false,
};
case "yunbrowser_screenshot": {
// 检查页面是否已关闭
if (session!.page.isClosed()) {
return {
content: [
{
type: "text",
text: `Page has been closed`,
},
],
isError: true,
};
}
const screenshot = await session!.page.screenshot({
encoding: "base64",
fullPage: false,
});
if (!screenshot) {
return {
content: [
{
type: "text",
text: "Screenshot failed",
},
],
isError: true,
};
}
screenshots.set(args.name, screenshot as string);
server.notification({
method: "notifications/resources/list_changed",
});
return {
content: [
{
type: "text",
text: `Screenshot taken`,
} as TextContent,
{
type: "image",
data: screenshot,
mimeType: "image/png",
} as ImageContent,
],
isError: false,
};
}
case "yunbrowser_click":
try {
// 检查页面是否已关闭
if (session!.page.isClosed()) {
return {
content: [
{
type: "text",
text: `Page has been closed`,
},
],
isError: true,
};
}
await session!.page.click(args.selector);
return {
content: [
{
type: "text",
text: `Clicked: ${args.selector}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to click ${args.selector}: ${
(error as Error).message
}`,
},
],
isError: true,
};
}
case "yunbrowser_hover":
// 检查页面是否已关闭
if (session!.page.isClosed()) {
return {
content: [
{
type: "text",
text: `Page has been closed`,
},
],
isError: true,
};
}
try {
await session!.page.hover(args.selector);
return {
content: [
{
type: "text",
text: `Hovered: ${args.selector}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to hover: ${args.selector}: ${
(error as Error).message
}`,
},
],
isError: true,
};
}
case "yunbrowser_fill":
// 检查页面是否已关闭
if (session!.page.isClosed()) {
return {
content: [
{
type: "text",
text: `Page has been closed`,
},
],
isError: true,
};
}
try {
await session!.page.waitForSelector(args.selector);
await session!.page.type(args.selector, args.value);
return {
content: [
{
type: "text",
text: `Filled ${args.selector} with: ${args.value}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to fill ${args.selector}: ${
(error as Error).message
}`,
},
],
isError: true,
};
}
case "yunbrowser_get_text": {
try {
// 检查页面是否已关闭
if (session!.page.isClosed()) {
return {
content: [
{
type: "text",
text: `Page has been closed`,
},
],
isError: true,
};
}
const bodyText = await session!.page.evaluate(
() => document.body.innerText
);
const content = bodyText
.split("\n")
.map((line) => line.trim())
.filter((line) => {
if (!line) return false;
if (
(line.includes("{") && line.includes("}")) ||
line.includes("@keyframes") || // Remove CSS animations
line.match(/^\.[a-zA-Z0-9_-]+\s*{/) || // Remove CSS lines starting with .className {
line.match(/^[a-zA-Z-]+:[a-zA-Z0-9%\s\(\)\.,-]+;$/) // Remove lines like "color: blue;" or "margin: 10px;"
) {
return false;
}
return true;
})
.map((line) => {
return line.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) =>
String.fromCharCode(parseInt(hex, 16))
);
});
return {
content: [
{
type: "text",
text: `Extracted content:\n${content.join("\n")}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Failed to extract content: ${(error as Error).message}`,
},
],
isError: true,
};
}
}
default:
return {
content: [
{
type: "text",
text: `Unknown tool: ${name}`,
},
],
isError: true,
};
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error(`Failed to handle tool call: ${errorMsg}`);
return {
content: [
{
type: "text",
text: `Failed to handle tool call: ${errorMsg}`,
},
],
isError: true,
};
}
}
// 6. Server Setup and Configuration
const server = new Server(
{
name: "example-servers/browserbase",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
// 7. Request Handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
...Array.from(screenshots.keys()).map((name) => ({
uri: `screenshot://${name}`,
mimeType: "image/png",
name: `Screenshot: ${name}`,
})),
],
}));
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri.toString();
if (uri.startsWith("screenshot://")) {
const name = uri.split("://")[1];
const screenshot = screenshots.get(name);
if (screenshot) {
return {
contents: [
{
uri,
mimeType: "image/png",
blob: screenshot,
},
],
};
}
}
throw new Error(`Resource not found: ${uri}`);
});
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: TOOLS,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) =>
handleToolCall(request.params.name, request.params.arguments ?? {})
);
// 8. Server Initialization
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch(console.error);