This commit is contained in:
kirigaya 2025-05-11 20:31:47 +00:00
parent 0525539ad8
commit 72182b32f5
41 changed files with 7425 additions and 3095 deletions

View File

@ -1,58 +0,0 @@
import '../plugins/image';
import { mapper, plugins, LagrangeContext, PrivateMessage, GroupMessage, Send, logger } from 'lagrange.onebot'
import { apiGetIntentRecogition, apiQueryVecdb } from '../api/vecdb';
import { handleGroupIntent } from './intent';
export class Impl {
@mapper.onPrivateUser(1193466151)
@plugins.use('echo')
@plugins.use('pm')
@plugins.use('wget-image')
async handleJinhui(c: LagrangeContext<PrivateMessage>) {
c.sendMessage([{
type: 'image',
data: {
file: 'file:///data/zhelonghuang/project/rag-llm/images/bird.png',
timeout: 10000
}
}])
c.finishSession();
}
@mapper.onGroup(956419963, { at: false })
async handleTestGroup(c: LagrangeContext<GroupMessage>) {
const texts = [];
const message = c.message;
for (const msg of message.message) {
if (msg.type === 'text') {
texts.push(msg.data.text);
}
}
const reply: Send.Default[] = [];
const axiosRes = await apiGetIntentRecogition({ query: texts.join('\n') });
const res = axiosRes.data;
if (res.code == 200) {
const intentResult = res.data;
// 如果 不确定性 太高,就将意图修改为
if (intentResult.uncertainty >= 0.33) {
intentResult.name = 'others';
}
const uncertainty = Math.round(intentResult.uncertainty * 1000) / 1000;
const intentDebug = `【意图: ${intentResult.name} 不确定度: ${uncertainty}`;
const anwser = await handleGroupIntent(c, intentResult);
if (anwser === undefined) {
c.sendMessage('拒答' + '\n' + intentDebug);
} else {
c.sendMessage(anwser + '\n' + intentDebug);
}
} else {
c.sendMessage('RAG 系统目前离线');
}
}
}

4
config.yaml Normal file
View File

@ -0,0 +1,4 @@
mcp-server:
port: 25565
bot:
port: 25564

View File

@ -1 +0,0 @@
scp -r config ubuntu@101.43.239.71:/home/ubuntu/files/data/llm-rag

View File

@ -1 +0,0 @@
{"query": "真的开线程是要tcl指令去改的"}

4
node/Lagrange.Core/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.json
*.db
*.png
*.OneBot

View File

@ -0,0 +1,5 @@
```bash
wget -c https://github.com/LagrangeDev/Lagrange.Core/releases/download/nightly/Lagrange.OneBot_linux-x64_net9.0_SelfContained.tar.gz
tar -xvf Lagrange.OneBot_linux-x64_net9.0_SelfContained.tar.gz && rm *.tar.gz
```

Binary file not shown.

Binary file not shown.

1
node/bot/README.md Normal file
View File

@ -0,0 +1 @@
执行 mcp 部分的模块

View File

@ -0,0 +1,2 @@
addr: 127.0.0.1
port: 8081

File diff suppressed because it is too large Load Diff

1
node/bot/src/README.md Normal file
View File

@ -0,0 +1 @@

View File

@ -8,6 +8,7 @@ interface LlmMessageItem {
content: string
}
// 已经废弃
class ErineLLM {
apiKey: string = process.env.BAIDU_API_KEY;
secretKey: string = process.env.BAIDU_SECRET_KEY;
@ -21,7 +22,7 @@ class ErineLLM {
throw Error('百度 secret_key 为空');
}
this.getAccessToken();
// this.getAccessToken();
}
public async getAccessToken() {

View File

@ -2,11 +2,6 @@ import * as fs from 'fs';
import { server } from 'lagrange.onebot';
import './services/test';
import './services/digital-ide';
const buffer = fs.readFileSync('./app/publish/appsettings.json', 'utf-8');
const config = JSON.parse(buffer);
const impl = config.Implementations[0];
server.onMounted(c => {
c.sendPrivateMsg(1193466151, '成功上线');
@ -16,6 +11,11 @@ server.onUnmounted(c => {
c.sendPrivateMsg(1193466151, '成功下线');
});
const buffer = fs.readFileSync('../Lagrange.Core/appsettings.json', 'utf-8');
const config = JSON.parse(buffer);
const impl = config.Implementations[0];
server.run({
host: impl.Host,
port: impl.Port,

View File

@ -8,6 +8,8 @@ import { handleGroupIntent } from './intent';
let lastCall = undefined;
console.log('activate ' + __filename);
export class Impl {
@mapper.onGroup(932987873, { at: false })
async handleDigitalGroup(c: LagrangeContext<GroupMessage>) {

View File

View File

@ -0,0 +1,87 @@
import '../plugins/image';
import axios from 'axios';
import { mapper, plugins, LagrangeContext, PrivateMessage, GroupMessage, Send, logger } from 'lagrange.onebot'
import { apiGetIntentRecogition, apiQueryVecdb } from '../api/vecdb';
import { handleGroupIntent } from './intent';
console.log('activate ' + __filename);
export class Impl {
@mapper.onPrivateUser(1193466151)
@plugins.use('echo')
@plugins.use('pm')
@plugins.use('wget-image')
async handleJinhui(c: LagrangeContext<PrivateMessage>) {
const text = c.getRawText();
console.log('[receive] ' + text);
if (text.startsWith(':')) {
const command = text.substring(1);
switch (command) {
case 'news':
await getNews(c);
break;
default:
break;
}
}
// c.sendMessage([{
// type: 'image',
// data: {
// file: 'file:///data/zhelonghuang/project/rag-llm/images/bird.png',
// timeout: 10000
// }
// }])
c.finishSession();
}
// @mapper.onGroup(956419963, { at: false })
// async handleTestGroup(c: LagrangeContext<GroupMessage>) {
// const texts = [];
// const message = c.message;
// for (const msg of message.message) {
// if (msg.type === 'text') {
// texts.push(msg.data.text);
// }
// }
// const reply: Send.Default[] = [];
// const axiosRes = await apiGetIntentRecogition({ query: texts.join('\n') });
// const res = axiosRes.data;
// if (res.code == 200) {
// const intentResult = res.data;
// // 如果 不确定性 太高,就将意图修改为
// if (intentResult.uncertainty >= 0.33) {
// intentResult.name = 'others';
// }
// const uncertainty = Math.round(intentResult.uncertainty * 1000) / 1000;
// const intentDebug = `【意图: ${intentResult.name} 不确定度: ${uncertainty}】`;
// const anwser = await handleGroupIntent(c, intentResult);
// if (anwser === undefined) {
// c.sendMessage('拒答' + '\n' + intentDebug);
// } else {
// c.sendMessage(anwser + '\n' + intentDebug);
// }
// } else {
// c.sendMessage('RAG 系统目前离线');
// }
// }
}
async function getNews(c: LagrangeContext<PrivateMessage>) {
const res = await axios.post('http://localhost:3000/get-news-from-towards-science');
const data = res.data;
const message = data.msg;
console.log('message', message);
c.sendMessage(message);
}

View File

@ -10,6 +10,6 @@
"typeRoots": ["./types"]
},
"include": [
"bot/**/*"
"src/**/*"
]
}

View File

@ -0,0 +1 @@
执行 mcp 部分的模块

2402
node/mcp-server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
{
"name": "mcp-server",
"version": "1.0.0",
"description": "执行 mcp 部分的模块",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^2.2.0",
"bson": "^6.10.3",
"cors": "^2.8.5",
"express": "^5.1.0",
"morgan": "^1.10.0",
"openmcp-sdk": "^0.0.4"
},
"devDependencies": {
"@types/cors": "^2.8.18",
"@types/express": "^5.0.1",
"@types/morgan": "^1.9.9",
"@types/node": "^22.15.17",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
}

View File

@ -0,0 +1,13 @@
import express, { Request, Response } from 'express';
import morgan from 'morgan';
import cors from 'cors';
const corsOptions = {
// 一些旧版浏览器(如 IE11、各种 SmartTV在 204 状态下会有问题
optionsSuccessStatus: 200
};
export const app = express();
app.use(express.json());
app.use(cors(corsOptions));
app.use(morgan('dev'));

View File

@ -0,0 +1,25 @@
import { Request, Response } from 'express';
import { app } from './common';
import "./service/news";
// 路由为 localhost:3000
app.get('/', (req: Request, res: Response) => {
res.send('<h1>Hello, World!</h1><br><img src="https://picx.zhimg.com/v2-b4251de7d2499e942c7ebf447a90d2eb_l.jpg"/>');
});
app.post('/hello', async (req: Request, res: Response) => {
try {
res.send('message');
} catch (error) {
console.log('error happen in /save-view, ' + error);
res.send('error')
}
})
// 运行在 3000 端口
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

View File

@ -0,0 +1,25 @@
import { TaskLoop } from 'openmcp-sdk/task-loop';
import { TaskLoopAdapter } from 'openmcp-sdk/service';
export async function createTaskContext() {
const adapter = new TaskLoopAdapter();
await adapter.connectMcpServer({
connectionType: 'STDIO',
command: 'node',
args: ['~/project/Lagrange.RagBot/node/servers/my-browser/dist/browser.js']
});
const taskLoop = new TaskLoop({ adapter });
taskLoop.setLlmConfig({
id: 'deepseek',
baseUrl: 'https://api.deepseek.com/v1',
userToken: process.env['DEEPSEEK_API_TOKEN'],
userModel: 'deepseek-chat'
});
return {
taskLoop, adapter
}
}

View File

@ -0,0 +1,57 @@
import { Request, Response } from 'express';
import { app } from "../common";
import { createTaskContext } from "./common";
export async function getNewsFromTowardsScience() {
const { taskLoop, adapter } = await createTaskContext();
const tools = await adapter.listTools();
// 创建当前事件循环对应的上下文,并且配置当前上下文的设置
const storage = {
messages: [],
settings: {
temperature: 0.7,
enableTools: tools,
systemPrompt: 'you are a clever bot',
contextLength: 20
}
};
// 本次发出的问题
const message = `
headless https://towardsdatascience.com/tag/editors-pick/ 最热门的前三条信息,这些信息大概率在 <main> 元素的 ul li 下面。然后帮我进入这些网站后总结相关信息,文章通常也在 <main> 中,并且整理成一个简单的咨询,返回翻译好的简体中文。比如
K1
{}
{}
广`.trim();
// 开启事件循环
await taskLoop.start(storage, message);
// 打印上下文,最终的回答在 messages.at(-1) 中
console.log(storage.messages);
const lastMessage = storage.messages.at(-1);
if (lastMessage?.role === 'assistant') {
return lastMessage.content;
}
return '';
}
app.post('/get-news-from-towards-science', async (req: Request, res: Response) => {
try {
const news = await getNewsFromTowardsScience();
res.send({
code: 200,
msg: news
});
} catch (error) {
res.send(error);
}
});

View File

@ -0,0 +1,7 @@
import { getNewsFromTowardsScience } from "../service/news";
async function main() {
const res = await getNewsFromTowardsScience();
}
main();

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"module": "CommonJS",
"target": "ES2020",
"outDir": "dist",
"esModuleInterop": true,
"experimentalDecorators": true,
"declaration": true,
"declarationDir": "dist",
"typeRoots": ["./types"]
},
"include": [
"src/**/*"
]
}

1
node/servers/my-browser/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node

View File

@ -0,0 +1,213 @@
# Puppeteer
A Model Context Protocol server that provides browser automation capabilities using Puppeteer. This server enables LLMs to interact with web pages, take screenshots, and execute JavaScript in a real browser environment.
## Components
### Tools
- **puppeteer_navigate**
- Navigate to any URL in the browser
- Inputs:
- `url` (string, required): URL to navigate to
- `launchOptions` (object, optional): PuppeteerJS LaunchOptions. Default null. If changed and not null, browser restarts. Example: `{ headless: true, args: ['--user-data-dir="C:/Data"'] }`
- `allowDangerous` (boolean, optional): Allow dangerous LaunchOptions that reduce security. When false, dangerous args like `--no-sandbox`, `--disable-web-security` will throw errors. Default false.
- **puppeteer_screenshot**
- Capture screenshots of the entire page or specific elements
- Inputs:
- `name` (string, required): Name for the screenshot
- `selector` (string, optional): CSS selector for element to screenshot
- `width` (number, optional, default: 800): Screenshot width
- `height` (number, optional, default: 600): Screenshot height
- **puppeteer_click**
- Click elements on the page
- Input: `selector` (string): CSS selector for element to click
- **puppeteer_hover**
- Hover elements on the page
- Input: `selector` (string): CSS selector for element to hover
- **puppeteer_fill**
- Fill out input fields
- Inputs:
- `selector` (string): CSS selector for input field
- `value` (string): Value to fill
- **puppeteer_select**
- Select an element with SELECT tag
- Inputs:
- `selector` (string): CSS selector for element to select
- `value` (string): Value to select
- **puppeteer_evaluate**
- Execute JavaScript in the browser console
- Input: `script` (string): JavaScript code to execute
### Resources
The server provides access to two types of resources:
1. **Console Logs** (`console://logs`)
- Browser console output in text format
- Includes all console messages from the browser
2. **Screenshots** (`screenshot://<name>`)
- PNG images of captured screenshots
- Accessible via the screenshot name specified during capture
## Key Features
- Browser automation
- Console log monitoring
- Screenshot capabilities
- JavaScript execution
- Basic web interaction (navigation, clicking, form filling)
- Customizable Puppeteer launch options
## Configuration to use Puppeteer Server
### Usage with Claude Desktop
Here's the Claude Desktop configuration to use the Puppeter server:
### Docker
**NOTE** The docker implementation will use headless chromium, where as the NPX version will open a browser window.
```json
{
"mcpServers": {
"puppeteer": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"--init",
"-e",
"DOCKER_CONTAINER=true",
"mcp/puppeteer"
]
}
}
}
```
### NPX
```json
{
"mcpServers": {
"puppeteer": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
}
}
}
```
### Usage with VS Code
For quick installation, use one of the one-click install buttons below...
[![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-NPM-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=puppeteer&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-puppeteer%22%5D%7D) [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-NPM-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=puppeteer&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-puppeteer%22%5D%7D&quality=insiders)
[![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Docker-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=puppeteer&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22--init%22%2C%22-e%22%2C%22DOCKER_CONTAINER%3Dtrue%22%2C%22mcp%2Fpuppeteer%22%5D%7D) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Docker-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=puppeteer&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22--init%22%2C%22-e%22%2C%22DOCKER_CONTAINER%3Dtrue%22%2C%22mcp%2Fpuppeteer%22%5D%7D&quality=insiders)
For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file.
For NPX installation (opens a browser window):
```json
{
"mcp": {
"servers": {
"puppeteer": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
}
}
}
}
```
For Docker installation (uses headless chromium):
```json
{
"mcp": {
"servers": {
"puppeteer": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"--init",
"-e",
"DOCKER_CONTAINER=true",
"mcp/puppeteer"
]
}
}
}
}
```
### Launch Options
You can customize Puppeteer's browser behavior in two ways:
1. **Environment Variable**: Set `PUPPETEER_LAUNCH_OPTIONS` with a JSON-encoded string in the MCP configuration's `env` parameter:
```json
{
"mcpServers": {
"mcp-puppeteer": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"],
"env": {
"PUPPETEER_LAUNCH_OPTIONS": "{ \"headless\": false, \"executablePath\": \"C:/Program Files/Google/Chrome/Application/chrome.exe\", \"args\": [] }",
"ALLOW_DANGEROUS": "true"
}
}
}
}
```
2. **Tool Call Arguments**: Pass `launchOptions` and `allowDangerous` parameters to the `puppeteer_navigate` tool:
```json
{
"url": "https://example.com",
"launchOptions": {
"headless": false,
"defaultViewport": { "width": 1280, "height": 720 }
}
}
```
## Build
Docker build:
```bash
docker build -t mcp/puppeteer -f src/puppeteer/Dockerfile .
```
## License
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.

View File

@ -0,0 +1,489 @@
#!/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";
// Define the tools once to avoid repetition
const TOOLS: Tool[] = [
{
name: "k_navigate",
description: "Navigate to a URL",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL to navigate to" },
launchOptions: { type: "object", description: "PuppeteerJS LaunchOptions. Default null. If changed and not null, browser restarts. Example: { headless: true, args: ['--no-sandbox'] }" },
allowDangerous: { type: "boolean", description: "Allow dangerous LaunchOptions that reduce security. When false, dangerous args like --no-sandbox will throw errors. Default false." },
},
required: ["url"],
},
},
{
name: "k_screenshot",
description: "Take a screenshot of the current page or a specific element",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Name for the screenshot" },
selector: { type: "string", description: "CSS selector for element to screenshot" },
width: { type: "number", description: "Width in pixels (default: 800)" },
height: { type: "number", description: "Height in pixels (default: 600)" },
},
required: ["name"],
},
},
{
name: "k_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: "k_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: "k_select",
description: "Select an element on the page with Select tag",
inputSchema: {
type: "object",
properties: {
selector: { type: "string", description: "CSS selector for element to select" },
value: { type: "string", description: "Value to select" },
},
required: ["selector", "value"],
},
},
{
name: "k_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: "k_evaluate",
description: "Execute JavaScript in the browser console",
inputSchema: {
type: "object",
properties: {
script: { type: "string", description: "JavaScript code to execute" },
},
required: ["script"],
},
},
];
// Global state
let browser: Browser | null;
let page: Page | null;
const consoleLogs: string[] = [];
const screenshots = new Map<string, string>();
let previousLaunchOptions: any = null;
async function ensureBrowser({ launchOptions, allowDangerous }: any) {
const DANGEROUS_ARGS = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--single-process',
'--disable-web-security',
'--ignore-certificate-errors',
'--disable-features=IsolateOrigins',
'--disable-site-isolation-trials',
'--allow-running-insecure-content'
];
// Parse environment config safely
let envConfig = {};
try {
envConfig = JSON.parse(process.env.PUPPETEER_LAUNCH_OPTIONS || '{}');
} catch (error: any) {
console.warn('Failed to parse PUPPETEER_LAUNCH_OPTIONS:', error?.message || error);
}
launchOptions = launchOptions || { headless: true };
if (launchOptions.headless === undefined) {
launchOptions.headless = true;
}
// Deep merge environment config with user-provided options
const mergedConfig = deepMerge(envConfig, launchOptions || {});
// Security validation for merged config
if (mergedConfig?.args) {
const dangerousArgs = mergedConfig.args?.filter?.((arg: string) => DANGEROUS_ARGS.some((dangerousArg: string) => arg.startsWith(dangerousArg)));
if (dangerousArgs?.length > 0 && !(allowDangerous || (process.env.ALLOW_DANGEROUS === 'true'))) {
throw new Error(`Dangerous browser arguments detected: ${dangerousArgs.join(', ')}. Fround from environment variable and tool call argument. ` +
'Set allowDangerous: true in the tool call arguments to override.');
}
}
try {
if ((browser && !browser.connected) ||
(launchOptions && (JSON.stringify(launchOptions) != JSON.stringify(previousLaunchOptions)))) {
await browser?.close();
browser = null;
}
}
catch (error) {
browser = null;
}
previousLaunchOptions = launchOptions;
if (!browser) {
const npx_args = { headless: false }
const docker_args = { headless: true, args: ["--no-sandbox", "--single-process", "--no-zygote"] }
browser = await puppeteer.launch(deepMerge(
process.env.DOCKER_CONTAINER ? docker_args : npx_args,
mergedConfig
));
const pages = await browser.pages();
page = pages[0];
page.on("console", (msg) => {
const logEntry = `[${msg.type()}] ${msg.text()}`;
consoleLogs.push(logEntry);
server.notification({
method: "notifications/resources/updated",
params: { uri: "console://logs" },
});
});
}
return page!;
}
// Deep merge utility function
function deepMerge(target: any, source: any): any {
const output = Object.assign({}, target);
if (typeof target !== 'object' || typeof source !== 'object') return source;
for (const key of Object.keys(source)) {
const targetVal = target[key];
const sourceVal = source[key];
if (Array.isArray(targetVal) && Array.isArray(sourceVal)) {
// Deduplicate args/ignoreDefaultArgs, prefer source values
output[key] = [...new Set([
...(key === 'args' || key === 'ignoreDefaultArgs' ?
targetVal.filter((arg: string) => !sourceVal.some((launchArg: string) => arg.startsWith('--') && launchArg.startsWith(arg.split('=')[0]))) :
targetVal),
...sourceVal
])];
} else if (sourceVal instanceof Object && key in target) {
output[key] = deepMerge(targetVal, sourceVal);
} else {
output[key] = sourceVal;
}
}
return output;
}
declare global {
interface Window {
mcpHelper: {
logs: string[],
originalConsole: Partial<typeof console>,
}
}
}
export async function handleToolCall(name: string, args: any): Promise<CallToolResult> {
const page = await ensureBrowser(args);
switch (name) {
case "k_navigate":
await page.goto(args.url);
return {
content: [{
type: "text",
text: `Navigated to ${args.url}`,
}],
isError: false,
};
case "k_screenshot": {
const width = args.width ?? 800;
const height = args.height ?? 600;
await page.setViewport({ width, height });
const screenshot = await (args.selector ?
(await page.$(args.selector))?.screenshot({ encoding: "base64" }) :
page.screenshot({ encoding: "base64", fullPage: false }));
if (!screenshot) {
return {
content: [{
type: "text",
text: args.selector ? `Element not found: ${args.selector}` : "Screenshot failed",
}],
isError: true,
};
}
screenshots.set(args.name, screenshot as string);
server.notification({
method: "notifications/resources/list_changed",
});
return {
content: [
{
type: "text",
text: `Screenshot '${args.name}' taken at ${width}x${height}`,
} as TextContent,
{
type: "image",
data: screenshot,
mimeType: "image/png",
} as ImageContent,
],
isError: false,
};
}
case "k_click":
try {
await 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 "k_fill":
try {
await page.waitForSelector(args.selector);
await 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 "k_select":
try {
await page.waitForSelector(args.selector);
await page.select(args.selector, args.value);
return {
content: [{
type: "text",
text: `Selected ${args.selector} with: ${args.value}`,
}],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed to select ${args.selector}: ${(error as Error).message}`,
}],
isError: true,
};
}
case "k_hover":
try {
await page.waitForSelector(args.selector);
await 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 "k_evaluate":
try {
await page.evaluate(() => {
window.mcpHelper = {
logs: [],
originalConsole: { ...console },
};
['log', 'info', 'warn', 'error'].forEach(method => {
(console as any)[method] = (...args: any[]) => {
window.mcpHelper.logs.push(`[${method}] ${args.join(' ')}`);
(window.mcpHelper.originalConsole as any)[method](...args);
};
});
});
const result = await page.evaluate(args.script);
const logs = await page.evaluate(() => {
Object.assign(console, window.mcpHelper.originalConsole);
const logs = window.mcpHelper.logs;
delete (window as any).mcpHelper;
return logs;
});
return {
content: [
{
type: "text",
text: `Execution result:\n${JSON.stringify(result, null, 2)}\n\nConsole output:\n${logs.join('\n')}`,
},
],
isError: false,
};
} catch (error) {
return {
content: [{
type: "text",
text: `Script execution failed: ${(error as Error).message}`,
}],
isError: true,
};
}
default:
return {
content: [{
type: "text",
text: `Unknown tool: ${name}`,
}],
isError: true,
};
}
}
const server = new Server(
{
name: "my-browser",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
},
},
);
// Setup request handlers
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
resources: [
{
uri: "console://logs",
mimeType: "text/plain",
name: "Browser console logs",
},
...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 === "console://logs") {
return {
contents: [{
uri,
mimeType: "text/plain",
text: consoleLogs.join("\n"),
}],
};
}
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 ?? {})
);
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
runServer().catch(console.error);
process.stdin.on("close", () => {
console.error("Puppeteer MCP Server closed");
server.close();
});

2351
node/servers/my-browser/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
{
"name": "@modelcontextprotocol/server-puppeteer",
"version": "0.6.2",
"description": "MCP server for browser automation using Puppeteer",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/servers/issues",
"bin": {
"mcp-server-puppeteer": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.10.2",
"puppeteer": "^23.4.0"
},
"devDependencies": {
"shx": "^0.3.4",
"typescript": "^5.6.2"
}
}

View File

@ -0,0 +1,14 @@
import { handleToolCall } from "./browser";
async function main() {
const res = await handleToolCall(
'k_navigate',
{
url: 'https://towardsdatascience.com/tag/editors-pick/'
}
)
console.log(res);
}
main();

View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"outDir": "./dist",
"rootDir": ".",
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": [
"./**/*.ts",
"src/**/*"
],
"exclude": ["node_modules"]
}

View File

@ -1,5 +0,0 @@
Flask==3.0.3
gevent==24.2.1
langchain==0.2.1
langchain_community==0.2.1
Requests==2.32.2

2449
yarn.lock

File diff suppressed because it is too large Load Diff