-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
140 lines (124 loc) · 4.87 KB
/
index.js
File metadata and controls
140 lines (124 loc) · 4.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import "dotenv/config.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import http from "http";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import puppeteer from "puppeteer";
import * as cheerio from "cheerio";
import NodeCache from "node-cache";
const PORT = parseInt(process.env.PORT, 10);
if (isNaN(PORT) || PORT < 1 || PORT > 65535) {
console.error("Error: PORT environment variable must be set to a valid port number (1-65535).");
process.exit(1);
}
const cache = new NodeCache({
checkperiod: 120, // 2 minutes
stdTTL: 900, // 15 minutes
maxKeys: 100,
});
let browser;
try {
browser = await puppeteer.launch({
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
args: [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
"--user-data-dir=/tmp/chromium-data",
],
headless: true,
});
} catch (err) {
console.error("Failed to launch browser:", err.message);
console.error("Ensure Chromium is installed and PUPPETEER_EXECUTABLE_PATH is set if needed.");
process.exit(1);
}
browser.on("disconnected", () => {
console.error("Browser disconnected unexpectedly. Exiting for container restart.");
process.exit(1);
});
/**
* Registers all MCP tools on the given server instance.
* @param {import("@modelcontextprotocol/sdk/server/mcp.js").McpServer} server
* @returns {void}
*/
function registerTools(server) {
server.registerTool(
"visit_page",
{
description: "Get the HTML or clean text of a web page at a given URL",
inputSchema: {
url: z.string().url().refine(
(v) => /^https?:\/\//i.test(v),
{ message: "Only http and https URLs are supported" }
).describe("HTTP or HTTPS URL to visit"),
extractText: z.boolean().optional().default(false).describe("When true, returns clean readable text instead of raw HTML"),
},
},
async ({ url, extractText }) => {
const cacheKey = `${Buffer.from(url, "utf8").toString("base64")}-${extractText}`;
const cached = cache.get(cacheKey);
if (cached) return cached;
let page;
try {
page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080 });
await page.goto(url, { waitUntil: "networkidle2", timeout: 30000 });
const html = await page.content();
let returnValue;
if (!extractText) {
returnValue = { content: [{ type: "text", text: html }] };
} else {
const $ = cheerio.load(html);
$("script, style, noscript, nav, footer, header, aside, iframe, svg, img").remove();
const title = $("title").text().trim();
const body = $("body").text().replace(/\s+/g, " ").trim();
const text = `TITLE: ${title}\n\n${body}`.slice(0, 50000);
returnValue = { content: [{ type: "text", text }] };
}
cache.set(cacheKey, returnValue);
return returnValue;
} catch (err) {
console.error("[visit_page] Error visiting", url, err);
return {
content: [{ type: "text", text: `Error: ${err.message}` }],
isError: true,
};
} finally {
if (page) await page.close().catch((err) => console.error("[visit_page] Failed to close page:", err));
}
}
);
}
const app = http.createServer(async (req, res) => {
try {
if (req.method === "POST" && req.url === "/mcp") {
const server = new McpServer({ name: "browser-mcp", version: "1.0.0" });
registerTools(server);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless
});
res.on("close", () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res);
return;
}
if (req.method === "GET" && req.url === "/health") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ status: "ok" }));
return;
}
res.writeHead(404);
res.end("Not found");
} catch (err) {
console.error("[http] Unhandled error:", err);
if (!res.headersSent) {
res.writeHead(500);
res.end("Internal Server Error");
}
}
});
app.listen(PORT, () => {
console.log(`Browser MCP server listening on port ${PORT}`);
});