update
This commit is contained in:
parent
0525539ad8
commit
72182b32f5
@ -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
4
config.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
mcp-server:
|
||||
port: 25565
|
||||
bot:
|
||||
port: 25564
|
@ -1 +0,0 @@
|
||||
scp -r config ubuntu@101.43.239.71:/home/ubuntu/files/data/llm-rag
|
4
node/Lagrange.Core/.gitignore
vendored
Normal file
4
node/Lagrange.Core/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*.json
|
||||
*.db
|
||||
*.png
|
||||
*.OneBot
|
5
node/Lagrange.Core/README.md
Normal file
5
node/Lagrange.Core/README.md
Normal 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
|
||||
```
|
BIN
node/Lagrange.Core/lagrange-0-db/.realm
Normal file
BIN
node/Lagrange.Core/lagrange-0-db/.realm
Normal file
Binary file not shown.
BIN
node/Lagrange.Core/lagrange-0-db/.realm.lock
Normal file
BIN
node/Lagrange.Core/lagrange-0-db/.realm.lock
Normal file
Binary file not shown.
1
node/bot/README.md
Normal file
1
node/bot/README.md
Normal file
@ -0,0 +1 @@
|
||||
执行 mcp 部分的模块
|
2
node/bot/config/vecdb.yml
Normal file
2
node/bot/config/vecdb.yml
Normal file
@ -0,0 +1,2 @@
|
||||
addr: 127.0.0.1
|
||||
port: 8081
|
2198
package-lock.json → node/bot/package-lock.json
generated
2198
package-lock.json → node/bot/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
1
node/bot/src/README.md
Normal file
1
node/bot/src/README.md
Normal file
@ -0,0 +1 @@
|
||||
|
@ -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() {
|
@ -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,
|
@ -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>) {
|
0
node/bot/src/services/openmcp.ts
Normal file
0
node/bot/src/services/openmcp.ts
Normal file
87
node/bot/src/services/test.ts
Normal file
87
node/bot/src/services/test.ts
Normal 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);
|
||||
}
|
@ -10,6 +10,6 @@
|
||||
"typeRoots": ["./types"]
|
||||
},
|
||||
"include": [
|
||||
"bot/**/*"
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
1
node/mcp-server/README.md
Normal file
1
node/mcp-server/README.md
Normal file
@ -0,0 +1 @@
|
||||
执行 mcp 部分的模块
|
2402
node/mcp-server/package-lock.json
generated
Normal file
2402
node/mcp-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
node/mcp-server/package.json
Normal file
27
node/mcp-server/package.json
Normal 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"
|
||||
}
|
||||
}
|
13
node/mcp-server/src/common/index.ts
Normal file
13
node/mcp-server/src/common/index.ts
Normal 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'));
|
25
node/mcp-server/src/main.ts
Normal file
25
node/mcp-server/src/main.ts
Normal 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}`);
|
||||
});
|
25
node/mcp-server/src/service/common.ts
Normal file
25
node/mcp-server/src/service/common.ts
Normal 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
|
||||
}
|
||||
}
|
57
node/mcp-server/src/service/news.ts
Normal file
57
node/mcp-server/src/service/news.ts
Normal 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);
|
||||
}
|
||||
});
|
7
node/mcp-server/src/test/test.ts
Normal file
7
node/mcp-server/src/test/test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { getNewsFromTowardsScience } from "../service/news";
|
||||
|
||||
async function main() {
|
||||
const res = await getNewsFromTowardsScience();
|
||||
}
|
||||
|
||||
main();
|
15
node/mcp-server/tsconfig.json
Normal file
15
node/mcp-server/tsconfig.json
Normal 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
1
node/servers/my-browser/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node
|
213
node/servers/my-browser/README.md
Normal file
213
node/servers/my-browser/README.md
Normal 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...
|
||||
|
||||
[](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) [](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)
|
||||
|
||||
[](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) [](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.
|
489
node/servers/my-browser/browser.ts
Normal file
489
node/servers/my-browser/browser.ts
Normal 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
2351
node/servers/my-browser/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
node/servers/my-browser/package.json
Normal file
28
node/servers/my-browser/package.json
Normal 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"
|
||||
}
|
||||
}
|
14
node/servers/my-browser/test.ts
Normal file
14
node/servers/my-browser/test.ts
Normal 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();
|
19
node/servers/my-browser/tsconfig.json
Normal file
19
node/servers/my-browser/tsconfig.json
Normal 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"]
|
||||
}
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user