@solvvn/mcp-simple-browser 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -0
- package/dist/browser.d.ts +7 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +22 -0
- package/dist/browser.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/tools.d.ts +3 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +187 -0
- package/dist/tools.js.map +1 -0
- package/docker-compose.yml +10 -0
- package/package.json +34 -0
- package/src/browser.ts +28 -0
- package/src/index.ts +18 -0
- package/src/tools.ts +210 -0
- package/tsconfig.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# MCP Simple Browser
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server that provides browser automation tools powered by CloakBrowser.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Headless Browser**: Automatically launches a stealth browser via CloakBrowser
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Add to Claude Desktop
|
|
13
|
+
claude mcp add simple-browser npx @solvvn/mcp-simple-browser
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Installation (Manual)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install
|
|
20
|
+
# or
|
|
21
|
+
yarn install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### As MCP Server
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Development
|
|
30
|
+
yarn dev
|
|
31
|
+
|
|
32
|
+
# Production
|
|
33
|
+
yarn build
|
|
34
|
+
yarn start
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The server runs on stdio and can be connected to any MCP-compatible client (Claude Desktop, etc.).
|
|
38
|
+
|
|
39
|
+
### Claude Desktop Configuration
|
|
40
|
+
|
|
41
|
+
Add to your Claude Desktop config:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"simple-browser": {
|
|
47
|
+
"command": "node",
|
|
48
|
+
"args": ["/path/to/mcp-simple-browser/dist/index.js"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Available Tools
|
|
55
|
+
|
|
56
|
+
| Tool | Description |
|
|
57
|
+
|------|-------------|
|
|
58
|
+
| `browser_navigate` | Navigate to a URL and wait for it to load |
|
|
59
|
+
| `browser_screenshot` | Take a screenshot (returns base64) |
|
|
60
|
+
| `browser_save_screenshot` | Take a screenshot and save to file |
|
|
61
|
+
| `browser_click` | Click an element by CSS selector |
|
|
62
|
+
| `browser_type` | Type text into an input field |
|
|
63
|
+
| `browser_get_content` | Get HTML content of the page |
|
|
64
|
+
| `browser_get_text` | Get text content from page or element |
|
|
65
|
+
| `browser_evaluate` | Execute JavaScript in page context |
|
|
66
|
+
| `browser_search` | Search the web (Google, DuckDuckGo, Bing) |
|
|
67
|
+
| `browser_wait` | Wait for specified milliseconds |
|
|
68
|
+
| `browser_print_to_pdf` | Print page to PDF |
|
|
69
|
+
| `browser_close` | Close browser and cleanup |
|
|
70
|
+
|
|
71
|
+
## Examples
|
|
72
|
+
|
|
73
|
+
### Navigate and Screenshot
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
// Navigate to a page
|
|
77
|
+
await browser_navigate({ url: "https://example.com" })
|
|
78
|
+
|
|
79
|
+
// Take screenshot
|
|
80
|
+
await browser_screenshot({ fullPage: true })
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Search
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
// Search with default engine (DuckDuckGo)
|
|
87
|
+
await browser_search({ query: "TypeScript MCP" })
|
|
88
|
+
|
|
89
|
+
// Search with specific engine
|
|
90
|
+
await browser_search({ query: "CloakBrowser", engine: "google" })
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Interact with Elements
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
// Type in search box
|
|
97
|
+
await browser_type({ selector: "input[name='q']", text: "hello" })
|
|
98
|
+
|
|
99
|
+
// Click a button
|
|
100
|
+
await browser_click({ selector: "button[type='submit']" })
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Type check
|
|
107
|
+
yarn typecheck
|
|
108
|
+
|
|
109
|
+
# Build
|
|
110
|
+
yarn build
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { launch } from "cloakbrowser";
|
|
2
|
+
type Browser = Awaited<ReturnType<typeof launch>>;
|
|
3
|
+
type Page = Awaited<ReturnType<Browser["newPage"]>>;
|
|
4
|
+
export declare function getPage(): Promise<Page>;
|
|
5
|
+
export declare function closeBrowser(): Promise<void>;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,KAAK,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC;AAClD,KAAK,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;AASpD,wBAAsB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ7C;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAKlD"}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { launch } from "cloakbrowser";
|
|
2
|
+
let browserInstance = null;
|
|
3
|
+
let pageInstance = null;
|
|
4
|
+
function createBrowser() {
|
|
5
|
+
return launch({ headless: true, humanize: true });
|
|
6
|
+
}
|
|
7
|
+
export async function getPage() {
|
|
8
|
+
if (!browserInstance) {
|
|
9
|
+
browserInstance = await createBrowser();
|
|
10
|
+
}
|
|
11
|
+
if (!pageInstance) {
|
|
12
|
+
pageInstance = await browserInstance.newPage();
|
|
13
|
+
}
|
|
14
|
+
return pageInstance;
|
|
15
|
+
}
|
|
16
|
+
export async function closeBrowser() {
|
|
17
|
+
await pageInstance?.close().catch(() => { });
|
|
18
|
+
await browserInstance?.close().catch(() => { });
|
|
19
|
+
pageInstance = null;
|
|
20
|
+
browserInstance = null;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAKtC,IAAI,eAAe,GAAmB,IAAI,CAAC;AAC3C,IAAI,YAAY,GAAgB,IAAI,CAAC;AAErC,SAAS,aAAa;IACpB,OAAO,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO;IAC3B,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,eAAe,GAAG,MAAM,aAAa,EAAE,CAAC;IAC1C,CAAC;IACD,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,YAAY,GAAG,MAAM,eAAe,CAAC,OAAO,EAAE,CAAC;IACjD,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,MAAM,YAAY,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC5C,MAAM,eAAe,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC/C,YAAY,GAAG,IAAI,CAAC;IACpB,eAAe,GAAG,IAAI,CAAC;AACzB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as dotenv from "dotenv";
|
|
2
|
+
dotenv.config();
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { registerTools } from "./tools.js";
|
|
6
|
+
async function main() {
|
|
7
|
+
const server = new McpServer({ name: "simple-browser", version: "1.0.0" });
|
|
8
|
+
registerTools(server);
|
|
9
|
+
await server.connect(new StdioServerTransport());
|
|
10
|
+
console.error("SimpleBrowser MCP Server running on stdio");
|
|
11
|
+
}
|
|
12
|
+
main().catch((err) => {
|
|
13
|
+
console.error(err);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
});
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAC3E,aAAa,CAAC,MAAM,CAAC,CAAC;IACtB,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,KAAK,CAAC,2CAA2C,CAAC,CAAC;AAC7D,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA8LzE,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAiBrD"}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { writeFile } from "fs/promises";
|
|
3
|
+
import { closeBrowser, getPage } from "./browser.js";
|
|
4
|
+
function createTool(def) {
|
|
5
|
+
return def;
|
|
6
|
+
}
|
|
7
|
+
function success(extra) {
|
|
8
|
+
return { success: true, ...extra };
|
|
9
|
+
}
|
|
10
|
+
const SearchEngines = {
|
|
11
|
+
google: (q) => `https://www.google.com/search?q=${q}`,
|
|
12
|
+
duckduckgo: (q) => `https://duckduckgo.com/?q=${q}`,
|
|
13
|
+
bing: (q) => `https://www.bing.com/search?q=${q}`,
|
|
14
|
+
};
|
|
15
|
+
const tools = [
|
|
16
|
+
createTool({
|
|
17
|
+
name: "browser_navigate",
|
|
18
|
+
description: "Navigate to a URL and wait for it to load.",
|
|
19
|
+
schema: {
|
|
20
|
+
url: z.string().describe("The URL to navigate to"),
|
|
21
|
+
waitUntil: z
|
|
22
|
+
.enum(["load", "domcontentloaded", "networkidle", "commit"])
|
|
23
|
+
.default("networkidle"),
|
|
24
|
+
},
|
|
25
|
+
handler: async ({ url, waitUntil }) => {
|
|
26
|
+
const page = await getPage();
|
|
27
|
+
await page.goto(url, { waitUntil });
|
|
28
|
+
return success({ url: page.url(), title: await page.title() });
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
createTool({
|
|
32
|
+
name: "browser_screenshot",
|
|
33
|
+
description: "Take a screenshot of the current page.",
|
|
34
|
+
schema: {
|
|
35
|
+
fullPage: z.boolean().default(false).describe("Capture full scrollable page"),
|
|
36
|
+
},
|
|
37
|
+
handler: async ({ fullPage }) => {
|
|
38
|
+
const buffer = await (await getPage()).screenshot({ fullPage });
|
|
39
|
+
return success({ format: "png", data: buffer.toString("base64") });
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
createTool({
|
|
43
|
+
name: "browser_click",
|
|
44
|
+
description: "Click an element by CSS selector.",
|
|
45
|
+
schema: { selector: z.string().describe("CSS selector") },
|
|
46
|
+
handler: async ({ selector }) => {
|
|
47
|
+
await (await getPage()).click(selector);
|
|
48
|
+
return success({});
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
createTool({
|
|
52
|
+
name: "browser_type",
|
|
53
|
+
description: "Type text into an input field.",
|
|
54
|
+
schema: {
|
|
55
|
+
selector: z.string(),
|
|
56
|
+
text: z.string(),
|
|
57
|
+
delay: z.number().default(50).describe("Delay between keystrokes (ms)"),
|
|
58
|
+
},
|
|
59
|
+
handler: async ({ selector, text, delay }) => {
|
|
60
|
+
await (await getPage()).type(selector, text, { delay });
|
|
61
|
+
return success({});
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
createTool({
|
|
65
|
+
name: "browser_get_content",
|
|
66
|
+
description: "Get the HTML content of the page.",
|
|
67
|
+
schema: {},
|
|
68
|
+
handler: async () => ({ html: await (await getPage()).content() }),
|
|
69
|
+
}),
|
|
70
|
+
createTool({
|
|
71
|
+
name: "browser_get_text",
|
|
72
|
+
description: "Get text content from the page or a specific element.",
|
|
73
|
+
schema: {
|
|
74
|
+
selector: z.string().optional().describe("CSS selector (defaults to body)"),
|
|
75
|
+
},
|
|
76
|
+
handler: async ({ selector }) => {
|
|
77
|
+
const el = selector || "body";
|
|
78
|
+
const text = await (await getPage()).$eval(el, (el) => el.textContent);
|
|
79
|
+
return { text: text ?? "" };
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
createTool({
|
|
83
|
+
name: "browser_evaluate",
|
|
84
|
+
description: "Execute JavaScript in the page context.",
|
|
85
|
+
schema: { script: z.string().describe("JavaScript code to execute") },
|
|
86
|
+
handler: async ({ script }) => ({ result: await (await getPage()).evaluate(script) }),
|
|
87
|
+
}),
|
|
88
|
+
createTool({
|
|
89
|
+
name: "browser_search",
|
|
90
|
+
description: "Search the web using a search engine.",
|
|
91
|
+
schema: {
|
|
92
|
+
query: z.string(),
|
|
93
|
+
engine: z.enum(["google", "duckduckgo", "bing"]).default("duckduckgo"),
|
|
94
|
+
},
|
|
95
|
+
handler: async ({ query, engine }) => {
|
|
96
|
+
const page = await getPage();
|
|
97
|
+
const searchUrl = SearchEngines[engine](encodeURIComponent(query));
|
|
98
|
+
await page.goto(searchUrl, { waitUntil: "networkidle" });
|
|
99
|
+
const results = await page.evaluate(() => {
|
|
100
|
+
const out = [];
|
|
101
|
+
const selectors = ["div.g a[href^='http']", "div[data-srg] a[href^='http']", ".result a[href^='http']"];
|
|
102
|
+
for (const sel of selectors) {
|
|
103
|
+
document.querySelectorAll(sel).forEach((a) => {
|
|
104
|
+
if (!a.href || /google|bing|duckduckgo/.test(a.href))
|
|
105
|
+
return;
|
|
106
|
+
const titleEl = a.querySelector("h3") || a;
|
|
107
|
+
out.push({
|
|
108
|
+
title: titleEl.textContent?.trim() ?? "",
|
|
109
|
+
url: a.href,
|
|
110
|
+
snippet: a.textContent?.trim() ?? "",
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
if (out.length)
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
return out.slice(0, 10);
|
|
117
|
+
});
|
|
118
|
+
return success({ engine, query, results });
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
createTool({
|
|
122
|
+
name: "browser_wait",
|
|
123
|
+
description: "Wait for a specified time.",
|
|
124
|
+
schema: { milliseconds: z.number().describe("Time to wait in ms") },
|
|
125
|
+
handler: async ({ milliseconds }) => {
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
127
|
+
return success({ waited: milliseconds });
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
createTool({
|
|
131
|
+
name: "browser_close",
|
|
132
|
+
description: "Close the browser and clean up resources.",
|
|
133
|
+
schema: {},
|
|
134
|
+
handler: async () => {
|
|
135
|
+
await closeBrowser();
|
|
136
|
+
return success({});
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
createTool({
|
|
140
|
+
name: "browser_save_screenshot",
|
|
141
|
+
description: "Take a screenshot and save it to a file.",
|
|
142
|
+
schema: {
|
|
143
|
+
filepath: z.string().describe("Path where to save the screenshot"),
|
|
144
|
+
fullPage: z.boolean().default(false).describe("Capture full scrollable page"),
|
|
145
|
+
},
|
|
146
|
+
handler: async ({ filepath, fullPage }) => {
|
|
147
|
+
const buffer = await (await getPage()).screenshot({ fullPage });
|
|
148
|
+
await writeFile(filepath, buffer);
|
|
149
|
+
return success({ filepath, size: buffer.length });
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
createTool({
|
|
153
|
+
name: "browser_print_to_pdf",
|
|
154
|
+
description: "Print the current page to PDF and save to a file.",
|
|
155
|
+
schema: {
|
|
156
|
+
filepath: z.string().describe("Path where to save the PDF"),
|
|
157
|
+
landscape: z.boolean().default(false).describe("Use landscape orientation"),
|
|
158
|
+
printBackground: z.boolean().default(true).describe("Print background graphics"),
|
|
159
|
+
},
|
|
160
|
+
handler: async ({ filepath, landscape, printBackground }) => {
|
|
161
|
+
const pdf = await (await getPage()).pdf({
|
|
162
|
+
landscape,
|
|
163
|
+
printBackground,
|
|
164
|
+
format: "A4",
|
|
165
|
+
});
|
|
166
|
+
await writeFile(filepath, pdf);
|
|
167
|
+
return success({ filepath, size: pdf.length });
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
];
|
|
171
|
+
function toResult(value, isError = false) {
|
|
172
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
173
|
+
return { content: [{ type: "text", text }], ...(isError && { isError }) };
|
|
174
|
+
}
|
|
175
|
+
export function registerTools(server) {
|
|
176
|
+
for (const tool of tools) {
|
|
177
|
+
server.registerTool(tool.name, { description: tool.description, inputSchema: tool.schema }, async (args) => {
|
|
178
|
+
try {
|
|
179
|
+
return toResult(await tool.handler(args));
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
return toResult({ error: err instanceof Error ? err.message : String(err) }, true);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@solvvn/mcp-simple-browser",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"mcp-simple-browser": "dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"dev": "tsx watch src/index.ts",
|
|
19
|
+
"typecheck": "tsc --noEmit"
|
|
20
|
+
},
|
|
21
|
+
"author": "Huy Le Tien <huy@macbook.local>",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
25
|
+
"cloakbrowser": "^0.3.30",
|
|
26
|
+
"dotenv": "^17.4.2",
|
|
27
|
+
"zod": "^4.4.3"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^25.9.1",
|
|
31
|
+
"tsx": "^4.22.3",
|
|
32
|
+
"typescript": "^6.0.3"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { launch } from "cloakbrowser";
|
|
2
|
+
|
|
3
|
+
type Browser = Awaited<ReturnType<typeof launch>>;
|
|
4
|
+
type Page = Awaited<ReturnType<Browser["newPage"]>>;
|
|
5
|
+
|
|
6
|
+
let browserInstance: Browser | null = null;
|
|
7
|
+
let pageInstance: Page | null = null;
|
|
8
|
+
|
|
9
|
+
function createBrowser(): Promise<Browser> {
|
|
10
|
+
return launch({ headless: true, humanize: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function getPage(): Promise<Page> {
|
|
14
|
+
if (!browserInstance) {
|
|
15
|
+
browserInstance = await createBrowser();
|
|
16
|
+
}
|
|
17
|
+
if (!pageInstance) {
|
|
18
|
+
pageInstance = await browserInstance.newPage();
|
|
19
|
+
}
|
|
20
|
+
return pageInstance;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function closeBrowser(): Promise<void> {
|
|
24
|
+
await pageInstance?.close().catch(() => {});
|
|
25
|
+
await browserInstance?.close().catch(() => {});
|
|
26
|
+
pageInstance = null;
|
|
27
|
+
browserInstance = null;
|
|
28
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as dotenv from "dotenv";
|
|
2
|
+
dotenv.config();
|
|
3
|
+
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { registerTools } from "./tools.js";
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const server = new McpServer({ name: "simple-browser", version: "1.0.0" });
|
|
10
|
+
registerTools(server);
|
|
11
|
+
await server.connect(new StdioServerTransport());
|
|
12
|
+
console.error("SimpleBrowser MCP Server running on stdio");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
main().catch((err) => {
|
|
16
|
+
console.error(err);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
});
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { writeFile } from "fs/promises";
|
|
3
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { closeBrowser, getPage } from "./browser.js";
|
|
6
|
+
|
|
7
|
+
type ToolHandler<T extends z.ZodRawShape> = (args: z.infer<z.ZodObject<T>>) => Promise<unknown>;
|
|
8
|
+
|
|
9
|
+
interface ToolDefinition<S extends z.ZodRawShape> {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
schema: S;
|
|
13
|
+
handler: ToolHandler<S>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createTool<S extends z.ZodRawShape>(def: ToolDefinition<S>) {
|
|
17
|
+
return def as unknown as ToolDefinition<z.ZodRawShape>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function success<T extends Record<string, unknown>>(extra: T) {
|
|
21
|
+
return { success: true, ...extra };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const SearchEngines = {
|
|
25
|
+
google: (q: string) => `https://www.google.com/search?q=${q}`,
|
|
26
|
+
duckduckgo: (q: string) => `https://duckduckgo.com/?q=${q}`,
|
|
27
|
+
bing: (q: string) => `https://www.bing.com/search?q=${q}`,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
const tools = [
|
|
31
|
+
createTool({
|
|
32
|
+
name: "browser_navigate",
|
|
33
|
+
description: "Navigate to a URL and wait for it to load.",
|
|
34
|
+
schema: {
|
|
35
|
+
url: z.string().describe("The URL to navigate to"),
|
|
36
|
+
waitUntil: z
|
|
37
|
+
.enum(["load", "domcontentloaded", "networkidle", "commit"])
|
|
38
|
+
.default("networkidle"),
|
|
39
|
+
},
|
|
40
|
+
handler: async ({ url, waitUntil }) => {
|
|
41
|
+
const page = await getPage();
|
|
42
|
+
await page.goto(url, { waitUntil });
|
|
43
|
+
return success({ url: page.url(), title: await page.title() });
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
createTool({
|
|
47
|
+
name: "browser_screenshot",
|
|
48
|
+
description: "Take a screenshot of the current page.",
|
|
49
|
+
schema: {
|
|
50
|
+
fullPage: z.boolean().default(false).describe("Capture full scrollable page"),
|
|
51
|
+
},
|
|
52
|
+
handler: async ({ fullPage }) => {
|
|
53
|
+
const buffer = await (await getPage()).screenshot({ fullPage });
|
|
54
|
+
return success({ format: "png", data: buffer.toString("base64") });
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
createTool({
|
|
58
|
+
name: "browser_click",
|
|
59
|
+
description: "Click an element by CSS selector.",
|
|
60
|
+
schema: { selector: z.string().describe("CSS selector") },
|
|
61
|
+
handler: async ({ selector }) => {
|
|
62
|
+
await (await getPage()).click(selector);
|
|
63
|
+
return success({});
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
createTool({
|
|
67
|
+
name: "browser_type",
|
|
68
|
+
description: "Type text into an input field.",
|
|
69
|
+
schema: {
|
|
70
|
+
selector: z.string(),
|
|
71
|
+
text: z.string(),
|
|
72
|
+
delay: z.number().default(50).describe("Delay between keystrokes (ms)"),
|
|
73
|
+
},
|
|
74
|
+
handler: async ({ selector, text, delay }) => {
|
|
75
|
+
await (await getPage()).type(selector, text, { delay });
|
|
76
|
+
return success({});
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
createTool({
|
|
80
|
+
name: "browser_get_content",
|
|
81
|
+
description: "Get the HTML content of the page.",
|
|
82
|
+
schema: {},
|
|
83
|
+
handler: async () => ({ html: await (await getPage()).content() }),
|
|
84
|
+
}),
|
|
85
|
+
createTool({
|
|
86
|
+
name: "browser_get_text",
|
|
87
|
+
description: "Get text content from the page or a specific element.",
|
|
88
|
+
schema: {
|
|
89
|
+
selector: z.string().optional().describe("CSS selector (defaults to body)"),
|
|
90
|
+
},
|
|
91
|
+
handler: async ({ selector }) => {
|
|
92
|
+
const el = selector || "body";
|
|
93
|
+
const text = await (await getPage()).$eval(el, (el: HTMLElement) => el.textContent);
|
|
94
|
+
return { text: text ?? "" };
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
createTool({
|
|
98
|
+
name: "browser_evaluate",
|
|
99
|
+
description: "Execute JavaScript in the page context.",
|
|
100
|
+
schema: { script: z.string().describe("JavaScript code to execute") },
|
|
101
|
+
handler: async ({ script }) => ({ result: await (await getPage()).evaluate(script) }),
|
|
102
|
+
}),
|
|
103
|
+
createTool({
|
|
104
|
+
name: "browser_search",
|
|
105
|
+
description: "Search the web using a search engine.",
|
|
106
|
+
schema: {
|
|
107
|
+
query: z.string(),
|
|
108
|
+
engine: z.enum(["google", "duckduckgo", "bing"]).default("duckduckgo"),
|
|
109
|
+
},
|
|
110
|
+
handler: async ({ query, engine }) => {
|
|
111
|
+
const page = await getPage();
|
|
112
|
+
const searchUrl = SearchEngines[engine](encodeURIComponent(query));
|
|
113
|
+
await page.goto(searchUrl, { waitUntil: "networkidle" });
|
|
114
|
+
|
|
115
|
+
const results = await page.evaluate(() => {
|
|
116
|
+
const out: Array<{ title: string; url: string; snippet: string }> = [];
|
|
117
|
+
const selectors = ["div.g a[href^='http']", "div[data-srg] a[href^='http']", ".result a[href^='http']"];
|
|
118
|
+
|
|
119
|
+
for (const sel of selectors) {
|
|
120
|
+
document.querySelectorAll<HTMLAnchorElement>(sel).forEach((a) => {
|
|
121
|
+
if (!a.href || /google|bing|duckduckgo/.test(a.href)) return;
|
|
122
|
+
const titleEl = a.querySelector("h3") || a;
|
|
123
|
+
out.push({
|
|
124
|
+
title: titleEl.textContent?.trim() ?? "",
|
|
125
|
+
url: a.href,
|
|
126
|
+
snippet: a.textContent?.trim() ?? "",
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
if (out.length) break;
|
|
130
|
+
}
|
|
131
|
+
return out.slice(0, 10);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return success({ engine, query, results });
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
createTool({
|
|
138
|
+
name: "browser_wait",
|
|
139
|
+
description: "Wait for a specified time.",
|
|
140
|
+
schema: { milliseconds: z.number().describe("Time to wait in ms") },
|
|
141
|
+
handler: async ({ milliseconds }) => {
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
143
|
+
return success({ waited: milliseconds });
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
createTool({
|
|
147
|
+
name: "browser_close",
|
|
148
|
+
description: "Close the browser and clean up resources.",
|
|
149
|
+
schema: {},
|
|
150
|
+
handler: async () => {
|
|
151
|
+
await closeBrowser();
|
|
152
|
+
return success({});
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
createTool({
|
|
156
|
+
name: "browser_save_screenshot",
|
|
157
|
+
description: "Take a screenshot and save it to a file.",
|
|
158
|
+
schema: {
|
|
159
|
+
filepath: z.string().describe("Path where to save the screenshot"),
|
|
160
|
+
fullPage: z.boolean().default(false).describe("Capture full scrollable page"),
|
|
161
|
+
},
|
|
162
|
+
handler: async ({ filepath, fullPage }) => {
|
|
163
|
+
const buffer = await (await getPage()).screenshot({ fullPage });
|
|
164
|
+
await writeFile(filepath, buffer);
|
|
165
|
+
return success({ filepath, size: buffer.length });
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
createTool({
|
|
169
|
+
name: "browser_print_to_pdf",
|
|
170
|
+
description: "Print the current page to PDF and save to a file.",
|
|
171
|
+
schema: {
|
|
172
|
+
filepath: z.string().describe("Path where to save the PDF"),
|
|
173
|
+
landscape: z.boolean().default(false).describe("Use landscape orientation"),
|
|
174
|
+
printBackground: z.boolean().default(true).describe("Print background graphics"),
|
|
175
|
+
},
|
|
176
|
+
handler: async ({ filepath, landscape, printBackground }) => {
|
|
177
|
+
const pdf = await (await getPage()).pdf({
|
|
178
|
+
landscape,
|
|
179
|
+
printBackground,
|
|
180
|
+
format: "A4",
|
|
181
|
+
});
|
|
182
|
+
await writeFile(filepath, pdf);
|
|
183
|
+
return success({ filepath, size: pdf.length });
|
|
184
|
+
},
|
|
185
|
+
}),
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
function toResult(value: unknown, isError = false): CallToolResult {
|
|
189
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
190
|
+
return { content: [{ type: "text", text }], ...(isError && { isError }) };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function registerTools(server: McpServer): void {
|
|
194
|
+
for (const tool of tools) {
|
|
195
|
+
server.registerTool(
|
|
196
|
+
tool.name,
|
|
197
|
+
{ description: tool.description, inputSchema: tool.schema },
|
|
198
|
+
async (args) => {
|
|
199
|
+
try {
|
|
200
|
+
return toResult(await tool.handler(args as never));
|
|
201
|
+
} catch (err) {
|
|
202
|
+
return toResult(
|
|
203
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
204
|
+
true,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|