Initial commit: LAN gateway for browser-mcp with /mcp, /api, /docs endpoints

This commit is contained in:
antior
2026-09-22 22:11:14 +08:00
commit 5159031faf
7 changed files with 1867 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
token.txt
+96
View File
@@ -0,0 +1,96 @@
# browser-mcp-gateway
把本机的 [browser-mcp](https://github.com/Agent360dk/browser-mcp)(Chrome 扩展 + 本地 stdio MCP server)桥接到局域网,让其他主机远程驱动这台机器上的 Chrome 浏览器,用于网页爬取、自动化操作等场景。
## 架构
```
本机 MCP client ──stdio──┐
├─→ 本机 WS 中枢(127.0.0.1:9876) ─→ Chrome 扩展 ─→ Chrome
局域网主机 ──HTTP──→ 本网关 ──stdio──┘ (gateway 上游 spawn browser-mcp)
```
网关内部 spawn 一个 browser-mcp stdio 进程作为上游(自动重连),对外在同一端口暴露两类接口,所有接口均需 Bearer 认证(也支持 `?token=` 查询参数)。
## 接口
| 路径 | 说明 |
|---|---|
| `GET /` | HTML 使用说明页(含示例地址设置栏:默认当前访问 host:port,可自定义/重置) |
| `GET /docs` | HTML 格式 API 文档 |
| `GET /docs.json` | JSON 格式 API 文档(浏览器访问自动渲染为高亮 HTML,curl 获取原始 JSON) |
| `POST /mcp` | MCP Streamable HTTP 端点(无状态,仅 POST),供 MCP client 接入 |
| `GET /api/health` | 存活与上游状态 |
| `GET /api/tools` | 列出全部浏览器工具 |
| `POST /api/call` | 调用工具,body:`{"name": "工具名", "arguments": {...}}` |
## 快速开始
```bash
npm install
echo "你的token" > token.txt # 或使用环境变量 GATEWAY_TOKEN
npm start # 或双击 start-gateway.bat
```
默认监听 `0.0.0.0:9109`。
### 配置项(环境变量)
| 变量 | 默认值 | 说明 |
|---|---|---|
| `PORT` | `9109` | 监听端口 |
| `HOST` | `0.0.0.0` | 监听地址 |
| `GATEWAY_TOKEN` | 读取 `token.txt` | Bearer 认证 token |
| `BROWSER_MCP_CLI` | `%APPDATA%\npm\node_modules\@agent360\browser-mcp\bin\cli.js` | 上游 browser-mcp 入口 |
## 使用
### REST API(curl / 脚本)
```bash
TOKEN=你的token
curl -s http://10.1.1.6:9109/api/call \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"browser_navigate","arguments":{"url":"https://example.com"}}'
```
### MCP client(Kimi Code)
`~/.kimi-code/mcp.json` 的 `mcpServers` 中加入(保存后新建会话生效):
```json
"browser-remote": {
"url": "http://10.1.1.6:9109/mcp",
"headers": { "Authorization": "Bearer 你的token" }
}
```
### Claude Code
```bash
claude mcp add --transport http browser-remote http://10.1.1.6:9109/mcp \
--header "Authorization: Bearer 你的token"
```
## 注意事项
- **浏览器在网关所在机器上**:远程调用驱动的是本机 Chrome(Cookie、登录态共享),需保持 Chrome 的 browser-mcp 扩展处于连接状态。
- **会话隔离**:每个 MCP client 获得独立的浏览器会话(各自开标签页),互不干扰;但同一标签页不要两边同时操作。
- **防火墙**:Windows 需放行端口,建议限制来源网段(管理员 PowerShell):
```powershell
New-NetFirewallRule -DisplayName "browser-mcp-gateway" -Direction Inbound -Protocol TCP -LocalPort 9109 -RemoteAddress 10.1.1.0/24 -Action Allow
```
- **截图路径限制**:`browser_screenshot` 的 `path` 参数只能写在网关进程工作目录内(browser-mcp 的安全限制),远程调用请传相对路径。
- **Token 即全部权限**:请勿泄露到不可信网络;`token.txt` 已加入 `.gitignore`。
- 持久化运行可挂到 Windows 任务计划程序(开机执行 `start-gateway.bat`)。
## 文件
```
gateway.js # 网关主程序:HTTP 服务 + 认证 + stdio 上游管理 + MCP 代理
pages.js # /、/docs、/docs.json 三个页面的 HTML/JSON 渲染
package.json # 仅依赖 @modelcontextprotocol/sdk
token.txt # Bearer token(gitignore)
start-gateway.bat # Windows 启动脚本
```
+202
View File
@@ -0,0 +1,202 @@
// LAN gateway for the local browser-mcp stdio server.
// Endpoints (all require "Authorization: Bearer <token>"):
// POST /mcp - MCP Streamable HTTP (stateless), for MCP clients on the LAN
// GET /api/health - liveness + upstream status
// GET /api/tools - list available browser tools
// POST /api/call - { "name": "<tool>", "arguments": {...} } -> tool result JSON
// Config: PORT env (default 9109), token read from token.txt next to this file.
import http from 'node:http';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import {
CallToolRequestSchema,
GetPromptRequestSchema,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { renderDocs, renderDocsHtml, renderHome, renderJsonViewer } from './pages.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PORT = Number(process.env.PORT || 9109);
const HOST = process.env.HOST || '0.0.0.0';
const TOKEN = (process.env.GATEWAY_TOKEN || readFileSync(join(__dirname, 'token.txt'), 'utf8')).trim();
const BROWSER_MCP_CLI = process.env.BROWSER_MCP_CLI
|| 'C:\\Users\\ljq29\\AppData\\Roaming\\npm\\node_modules\\@agent360\\browser-mcp\\bin\\cli.js';
// ---------- upstream stdio client (spawns browser-mcp, reconnects on exit) ----------
let client = null;
let upstreamReady = false;
async function connectUpstream() {
const transport = new StdioClientTransport({
command: process.execPath,
args: [BROWSER_MCP_CLI],
stderr: 'pipe',
});
const c = new Client(
{ name: 'browser-mcp-gateway', version: '1.0.0' },
{ capabilities: {} },
);
transport.stderr?.on('data', (d) => console.error('[upstream]', d.toString().trim()));
c.onclose = () => {
upstreamReady = false;
console.error('[upstream] closed, reconnecting in 3s...');
setTimeout(() => connectUpstream().catch(console.error), 3000);
};
await c.connect(transport);
client = c;
upstreamReady = true;
console.log('[upstream] connected to browser-mcp stdio server');
}
function requireUpstream(res) {
if (!upstreamReady || !client) {
sendJson(res, 503, { error: 'upstream browser-mcp not ready' });
return false;
}
return true;
}
// ---------- MCP proxy server (per request, stateless) ----------
function buildProxyServer() {
const server = new Server(
{ name: 'browser-mcp-gateway', version: '1.0.0' },
{ capabilities: { tools: {}, resources: {}, prompts: {} } },
);
server.setRequestHandler(ListToolsRequestSchema, (req) => client.listTools(req.params));
server.setRequestHandler(CallToolRequestSchema, (req) => client.callTool(req.params));
server.setRequestHandler(ListResourcesRequestSchema, (req) => client.listResources(req.params));
server.setRequestHandler(ReadResourceRequestSchema, (req) => client.readResource(req.params));
server.setRequestHandler(ListResourceTemplatesRequestSchema, (req) => client.listResourceTemplates(req.params));
server.setRequestHandler(ListPromptsRequestSchema, (req) => client.listPrompts(req.params));
server.setRequestHandler(GetPromptRequestSchema, (req) => client.getPrompt(req.params));
return server;
}
async function handleMcp(req, res, body) {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
const server = buildProxyServer();
res.on('close', () => {
transport.close().catch(() => {});
server.close().catch(() => {});
});
await server.connect(transport);
await transport.handleRequest(req, res, body);
}
// ---------- HTTP plumbing ----------
function sendJson(res, status, obj) {
const data = JSON.stringify(obj);
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(data);
}
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
if (!chunks.length) return resolve(undefined);
try {
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
} catch (e) {
reject(new Error('invalid JSON body'));
}
});
req.on('error', reject);
});
}
function authed(req, url) {
const h = req.headers.authorization || '';
return h === `Bearer ${TOKEN}` || url.searchParams.get('token') === TOKEN;
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
const path = url.pathname.replace(/\/+$/, '') || '/';
try {
if (!authed(req, url)) {
res.writeHead(401, { 'WWW-Authenticate': 'Bearer', 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'unauthorized' }));
}
const baseUrl = `http://${req.headers.host || `localhost:${PORT}`}`;
if ((path === '/' || path === '') && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(renderHome(baseUrl, TOKEN));
}
if (path === '/docs' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(renderDocsHtml(baseUrl, TOKEN));
}
if (path === '/docs.json' && req.method === 'GET') {
const doc = renderDocs(baseUrl, TOKEN);
if ((req.headers.accept || '').includes('text/html')) {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(renderJsonViewer(baseUrl, TOKEN, doc));
}
return sendJson(res, 200, doc);
}
if (path === '/mcp') {
if (req.method !== 'POST') return sendJson(res, 405, { error: 'stateless /mcp only accepts POST' });
if (!requireUpstream(res)) return;
const body = await readBody(req);
return await handleMcp(req, res, body);
}
if (path === '/api/health' && req.method === 'GET') {
return sendJson(res, 200, { ok: true, upstream: upstreamReady });
}
if (path === '/api/tools' && req.method === 'GET') {
if (!requireUpstream(res)) return;
return sendJson(res, 200, await client.listTools());
}
if (path === '/api/call' && req.method === 'POST') {
if (!requireUpstream(res)) return;
const body = await readBody(req);
if (!body || typeof body.name !== 'string') {
return sendJson(res, 400, { error: 'body must be { "name": string, "arguments"?: object }' });
}
const result = await client.callTool({ name: body.name, arguments: body.arguments || {} });
return sendJson(res, 200, result);
}
sendJson(res, 404, { error: 'not found', routes: ['/', '/docs', '/docs.json', '/mcp', '/api/health', '/api/tools', '/api/call'] });
} catch (e) {
console.error('[error]', e);
if (!res.headersSent) sendJson(res, 500, { error: String(e?.message || e) });
else res.end();
}
});
server.listen(PORT, HOST, () => {
console.log(`browser-mcp gateway listening on http://${HOST}:${PORT} (routes: /mcp, /api/*)`);
});
connectUpstream().catch((e) => {
console.error('[upstream] initial connect failed, retrying in 3s:', e.message);
setTimeout(() => connectUpstream().catch(console.error), 3000);
});
+1202
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
{
"name": "browser-mcp-gateway",
"version": "1.0.0",
"description": "LAN gateway for local browser-mcp: exposes /mcp (MCP Streamable HTTP) and /api (REST) with Bearer auth",
"type": "module",
"main": "gateway.js",
"scripts": {
"start": "node gateway.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.30.0"
}
}
+349
View File
@@ -0,0 +1,349 @@
// HTML home page and JSON API docs for the browser-mcp gateway.
// All examples are built from the visitor's Host header so IP/port always match.
export function renderHome(baseUrl, token) {
// B is a placeholder element filled by client-side JS with the effective base
// URL (visitor's host by default, overridable in the settings bar).
const B = '<b class="bp"></b>';
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>browser-mcp gateway</title>
<style>
body { font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; width: 96%; max-width: 1400px; margin: 1.5em auto; padding: 0 1em; color: #24292f; line-height: 1.6; box-sizing: border-box; }
h1 { border-bottom: 2px solid #d0d7de; padding-bottom: .3em; }
h2 { margin-top: 1.8em; color: #0969da; }
pre { background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 12px; overflow-x: auto; font-size: 13px; }
code { background: #f6f8fa; padding: 2px 5px; border-radius: 4px; font-size: 13px; }
pre code { padding: 0; background: none; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #d0d7de; padding: 6px 10px; text-align: left; font-size: 14px; }
th { background: #f6f8fa; }
.warn { background: #fff8c5; border: 1px solid #d4a72c; border-radius: 6px; padding: 10px 14px; }
.bp { font-weight: inherit; }
.settings { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; background: #ddf4e4; border: 1px solid #1a7f37; border-radius: 8px; padding: 12px 16px; margin: 1em 0; }
.settings label { font-weight: 600; }
.settings input { flex: 1; min-width: 240px; padding: 6px 10px; border: 1px solid #d0d7de; border-radius: 6px; font-family: ui-monospace, Consolas, monospace; font-size: 13px; }
.settings input:focus { outline: 2px solid #0969da; outline-offset: -1px; }
.settings button { padding: 6px 16px; border-radius: 6px; border: 1px solid #1a7f37; background: #1a7f37; color: #fff; cursor: pointer; font-size: 13px; }
.settings button:hover { background: #15652e; }
.settings button.ghost { background: #fff; color: #24292f; border-color: #d0d7de; }
.settings button.ghost:hover { background: #f6f8fa; }
.settings .msg { color: #1a7f37; font-size: 13px; min-height: 1em; }
</style>
</head>
<body>
<h1>browser-mcp gateway</h1>
<div class="settings">
<label for="hostInput">示例地址</label>
<input id="hostInput" type="text" spellcheck="false" placeholder="host:port,例如 10.1.1.6:9109">
<button id="applyBtn">应用</button>
<button id="resetBtn" class="ghost">重置</button>
<span class="msg" id="barMsg"></span>
</div>
<p>本网关把本机 Chrome 浏览器(通过 browser-mcp 扩展)暴露到局域网。页面所有示例基于 <code>${B}</code> 生成(默认为你当前的访问地址,可在上方设置栏修改)。</p>
<h2>1. 认证</h2>
<p>所有接口都需要认证,二选一:</p>
<ul>
<li>HTTP 头:<code>Authorization: Bearer ${token}</code></li>
<li>URL 参数:<code>?token=${token}</code>(方便浏览器直接打开,例如 <a href="/docs?token=${token}">API 文档</a>)</li>
</ul>
<h2>2. 查看 API 文档</h2>
<p>HTML 文档:<a href="/docs?token=${token}"><code>${B}/docs</code></a> | JSON 版本:<a href="/docs.json?token=${token}"><code>${B}/docs.json</code></a></p>
<h2>3. REST API 使用(/api)</h2>
<table>
<tr><th>方法</th><th>路径</th><th>说明</th></tr>
<tr><td>GET</td><td>/api/health</td><td>存活与上游状态</td></tr>
<tr><td>GET</td><td>/api/tools</td><td>列出全部浏览器工具(40 个)</td></tr>
<tr><td>POST</td><td>/api/call</td><td>调用工具,body 为 <code>{"name": "工具名", "arguments": {...}}</code></td></tr>
</table>
<p>示例:打开网页并读取内容</p>
<pre><code>curl -s -X POST ${B}/api/call \\
-H "Authorization: Bearer ${token}" \\
-H "Content-Type: application/json" \\
-d '{"name":"browser_navigate","arguments":{"url":"https://example.com"}}'
curl -s -X POST ${B}/api/call \\
-H "Authorization: Bearer ${token}" \\
-H "Content-Type: application/json" \\
-d '{"name":"browser_get_page_content","arguments":{"format":"text"}}'</code></pre>
<p>返回格式:MCP 标准结果 <code>{"content":[{"type":"text","text":"..."}]}</code>,错误时 HTTP 状态码非 200 且 body 为 <code>{"error":"..."}</code>。</p>
<h2>4. 添加 MCP(/mcp 端点)</h2>
<p>MCP 端点地址:<code>${B}/mcp</code>(Streamable HTTP 传输,无状态模式)。</p>
<h3>Kimi Code</h3>
<p>编辑 <code>~/.kimi-code/mcp.json</code>,在 <code>mcpServers</code> 中加入:</p>
<pre><code>"browser-remote": {
"url": "${B}/mcp",
"headers": { "Authorization": "Bearer ${token}" }
}</code></pre>
<p>保存后<strong>新建会话</strong>生效(已打开的会话不会加载新增的 server)。也可以在 TUI 里用 <code>/mcp-config</code> 交互式添加。</p>
<h3>Claude Code</h3>
<pre><code>claude mcp add --transport http browser-remote ${B}/mcp \\
--header "Authorization: Bearer ${token}"</code></pre>
<h2>5. 一句话部署</h2>
<p>Linux / macOS(写入 Kimi Code 用户级配置):</p>
<pre><code>node -e "const f=process.env.HOME+'/.kimi-code/mcp.json',j=require(f);j.mcpServers['browser-remote']={url:'${B}/mcp',headers:{Authorization:'Bearer ${token}'}};require('fs').writeFileSync(f,JSON.stringify(j,null,2))"</code></pre>
<p>Windows PowerShell:</p>
<pre><code>$f="$env:USERPROFILE\\.kimi-code\\mcp.json"; $j=Get-Content $f -Raw | ConvertFrom-Json; $j.mcpServers | Add-Member -NotePropertyName browser-remote -NotePropertyValue @{url='${B}/mcp';headers=@{Authorization='Bearer ${token}'}} -Force; $j | ConvertTo-Json -Depth 10 | Set-Content $f -Encoding UTF8</code></pre>
<div class="warn">
<strong>注意:</strong>远程调用驱动的是网关所在机器的 Chrome,请保持该机器的 browser-mcp 扩展处于连接状态。Token 即全部权限,请勿泄露到不可信网络。
</div>
<script>
(function () {
var KEY = 'gw-custom-host';
var input = document.getElementById('hostInput');
var msg = document.getElementById('barMsg');
function curHost() { return localStorage.getItem(KEY) || location.host; }
function render() {
var base = 'http://' + curHost();
var els = document.querySelectorAll('.bp');
for (var i = 0; i < els.length; i++) els[i].textContent = base;
}
function flash(text) {
msg.textContent = text;
setTimeout(function () { msg.textContent = ''; }, 3000);
}
document.getElementById('applyBtn').onclick = function () {
var v = input.value.trim().replace(/^https?:\\/\\//, '').replace(/\\/+$/, '');
if (!v) { flash('请输入 host:port'); return; }
localStorage.setItem(KEY, v);
input.value = v;
render();
flash('已应用:' + v);
};
document.getElementById('resetBtn').onclick = function () {
localStorage.removeItem(KEY);
input.value = location.host;
render();
flash('已重置为当前访问地址');
};
input.value = curHost();
render();
})();
</script>
</body>
</html>`;
}
export function renderDocs(baseUrl, token) {
const auth = { type: 'bearer', header: 'Authorization: Bearer <token>', alt: 'query param ?token=<token>' };
return {
name: 'browser-mcp-gateway',
version: '1.0.0',
description: 'LAN gateway exposing the local browser-mcp (Agent360) browser over MCP Streamable HTTP and a simple REST API.',
baseUrl,
auth,
endpoints: [
{
method: 'GET', path: '/',
description: 'HTML 使用说明页(认证、REST API 用法、MCP 接入、一句话部署)。',
response: 'text/html',
},
{
method: 'GET', path: '/docs',
description: 'HTML 格式的 API 文档(本页)。',
response: 'text/html',
},
{
method: 'GET', path: '/docs.json',
description: 'JSON 格式的 API 文档(机器可读)。',
response: 'application/json',
},
{
method: 'GET', path: '/api/health',
description: 'Liveness probe; reports whether the upstream browser-mcp stdio server is connected.',
response: { ok: true, upstream: true },
example: `curl -s ${baseUrl}/api/health -H "Authorization: Bearer ${token}"`,
},
{
method: 'GET', path: '/api/tools',
description: 'List all available browser tools (MCP tools/list).',
response: { tools: [{ name: 'browser_navigate', description: '...', inputSchema: {} }] },
example: `curl -s ${baseUrl}/api/tools -H "Authorization: Bearer ${token}"`,
},
{
method: 'POST', path: '/api/call',
description: 'Call a browser tool (MCP tools/call). Body: {"name": string, "arguments"?: object}.',
request: { name: 'browser_navigate', arguments: { url: 'https://example.com' } },
response: { content: [{ type: 'text', text: '...' }] },
example: `curl -s -X POST ${baseUrl}/api/call -H "Authorization: Bearer ${token}" -H "Content-Type: application/json" -d '{"name":"browser_list_tabs","arguments":{}}'`,
},
{
method: 'POST', path: '/mcp',
description: 'MCP Streamable HTTP endpoint (stateless; POST only). Point any MCP client here.',
notes: 'Accept header must include application/json and text/event-stream per the MCP spec.',
},
],
mcpClients: {
'kimi-code': {
file: '~/.kimi-code/mcp.json',
config: { 'browser-remote': { url: `${baseUrl}/mcp`, headers: { Authorization: `Bearer ${token}` } } },
note: 'Takes effect in new sessions; or use /mcp-config in the TUI.',
},
'claude-code': {
oneLiner: `claude mcp add --transport http browser-remote ${baseUrl}/mcp --header "Authorization: Bearer ${token}"`,
},
},
oneLinerDeploy: {
posix: `node -e "const f=process.env.HOME+'/.kimi-code/mcp.json',j=require(f);j.mcpServers['browser-remote']={url:'${baseUrl}/mcp',headers:{Authorization:'Bearer ${token}'}};require('fs').writeFileSync(f,JSON.stringify(j,null,2))"`,
powershell: `$f="$env:USERPROFILE\\.kimi-code\\mcp.json"; $j=Get-Content $f -Raw | ConvertFrom-Json; $j.mcpServers | Add-Member -NotePropertyName browser-remote -NotePropertyValue @{url='${baseUrl}/mcp';headers=@{Authorization='Bearer ${token}'}} -Force; $j | ConvertTo-Json -Depth 10 | Set-Content $f -Encoding UTF8`,
},
errors: {
401: 'missing or wrong token',
400: 'bad request body',
404: 'unknown route',
503: 'upstream browser-mcp not connected (is the Chrome extension connected?)',
},
};
}
function esc(s) {
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
export function renderDocsHtml(baseUrl, token) {
const d = renderDocs(baseUrl, token);
const pre = (o) => `<pre><code>${esc(typeof o === 'string' ? o : JSON.stringify(o, null, 2))}</code></pre>`;
const endpointRows = d.endpoints.map((e) =>
`<tr><td><span class="m m-${e.method.toLowerCase()}">${e.method}</span></td><td><code>${e.path}</code></td><td>${esc(e.description)}</td></tr>`,
).join('\n');
const endpointDetails = d.endpoints.map((e) => `
<section class="card">
<h3><span class="m m-${e.method.toLowerCase()}">${e.method}</span> <code>${e.path}</code></h3>
<p>${esc(e.description)}</p>
${e.notes ? `<p class="note">${esc(e.notes)}</p>` : ''}
${e.request ? `<h4>请求 body</h4>${pre(e.request)}` : ''}
${e.response ? `<h4>响应${typeof e.response === 'string' ? '(' + esc(e.response) + ')' : ' 示例'}</h4>${pre(e.response)}` : ''}
${e.example ? `<h4>示例</h4>${pre(e.example)}` : ''}
</section>`).join('\n');
const errorRows = Object.entries(d.errors).map(([code, msg]) =>
`<tr><td><code>${code}</code></td><td>${esc(msg)}</td></tr>`,
).join('\n');
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>API 文档 - browser-mcp gateway</title>
<style>
body { font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; width: 96%; max-width: 1400px; margin: 1.5em auto; padding: 0 1em; color: #24292f; line-height: 1.6; box-sizing: border-box; }
h1 { border-bottom: 2px solid #d0d7de; padding-bottom: .3em; }
h2 { margin-top: 1.8em; color: #0969da; }
h3 { margin-bottom: .3em; }
h4 { margin: .8em 0 .3em; color: #57606a; font-size: 14px; }
pre { background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 12px; overflow-x: auto; font-size: 13px; }
code { background: #f6f8fa; padding: 2px 5px; border-radius: 4px; font-size: 13px; }
pre code { padding: 0; background: none; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #d0d7de; padding: 6px 10px; text-align: left; font-size: 14px; vertical-align: top; }
th { background: #f6f8fa; }
.card { border: 1px solid #d0d7de; border-radius: 8px; padding: 4px 18px 14px; margin: 14px 0; }
.m { display: inline-block; min-width: 3em; text-align: center; font-weight: 700; font-size: 12px; border-radius: 4px; padding: 2px 8px; color: #fff; }
.m-get { background: #1a7f37; }
.m-post { background: #0969da; }
.note { color: #57606a; font-size: 13px; }
.meta { color: #57606a; }
</style>
</head>
<body>
<h1>${esc(d.name)} <span class="meta">v${esc(d.version)}</span></h1>
<p>${esc(d.description)}</p>
<p>Base URL:<code>${esc(d.baseUrl)}</code> | <a href="${esc(d.baseUrl)}/?token=${esc(token)}">返回使用说明</a> | <a href="${esc(d.baseUrl)}/docs.json?token=${esc(token)}">JSON 版本</a></p>
<h2>认证</h2>
<p>HTTP 头:<code>${esc(d.auth.header)}</code>,或 URL 参数:<code>${esc(d.auth.alt)}</code></p>
<h2>端点总览</h2>
<table>
<tr><th>方法</th><th>路径</th><th>说明</th></tr>
${endpointRows}
</table>
<h2>端点详情</h2>
${endpointDetails}
<h2>MCP 客户端接入</h2>
<h3>Kimi Code(${esc(d.mcpClients['kimi-code'].file)})</h3>
${pre(JSON.stringify({ mcpServers: d.mcpClients['kimi-code'].config }, null, 2))}
<p class="note">${esc(d.mcpClients['kimi-code'].note)}</p>
<h3>Claude Code(一句话)</h3>
${pre(d.mcpClients['claude-code'].oneLiner)}
<h2>一句话部署(写入 Kimi Code 配置)</h2>
<h3>Linux / macOS</h3>
${pre(d.oneLinerDeploy.posix)}
<h3>Windows PowerShell</h3>
${pre(d.oneLinerDeploy.powershell)}
<h2>错误码</h2>
<table>
<tr><th>状态码</th><th>含义</th></tr>
${errorRows}
</table>
</body>
</html>`;
}
// Server-side JSON syntax highlighting for the /docs.json browser view.
function highlightJson(obj) {
const json = JSON.stringify(obj, null, 2);
const re = /("(?:\\.|[^"\\])*")(\s*:)?|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b|(true|false|null)\b/g;
let out = '';
let last = 0;
let m;
while ((m = re.exec(json))) {
out += esc(json.slice(last, m.index));
if (m[1] !== undefined) {
if (m[2]) out += `<span class="k">${esc(m[1])}</span>${esc(m[2])}`;
else out += `<span class="s">${esc(m[1])}</span>`;
} else if (m[3] !== undefined) {
out += `<span class="n">${esc(m[3])}</span>`;
} else {
out += `<span class="b">${esc(m[4])}</span>`;
}
last = m.index + m[0].length;
}
return out + esc(json.slice(last));
}
export function renderJsonViewer(baseUrl, token, doc) {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>docs.json - browser-mcp gateway</title>
<style>
body { font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; width: 96%; max-width: 1400px; margin: 1.5em auto; padding: 0 1em; color: #24292f; line-height: 1.6; box-sizing: border-box; }
h1 { border-bottom: 2px solid #d0d7de; padding-bottom: .3em; font-size: 22px; }
.meta { color: #57606a; font-size: 14px; }
pre { background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 6px; padding: 16px; overflow-x: auto; font-size: 13px; font-family: ui-monospace, Consolas, monospace; line-height: 1.5; }
.k { color: #0550ae; }
.s { color: #0a3069; }
.n { color: #0550ae; }
.b { color: #cf222e; }
</style>
</head>
<body>
<h1>docs.json <span class="meta">browser-mcp gateway API 文档(JSON)</span></h1>
<p class="meta">浏览器访问自动格式化高亮显示;用 curl 或 <code>Accept: application/json</code> 请求可获取原始 JSON。
| <a href="${esc(baseUrl)}/docs?token=${esc(token)}">返回 HTML 文档</a>
| <a href="${esc(baseUrl)}/?token=${esc(token)}">使用说明</a></p>
<pre>${highlightJson(doc)}</pre>
</body>
</html>`;
}
+3
View File
@@ -0,0 +1,3 @@
@echo off
cd /d "%~dp0"
node gateway.js