350 lines
16 KiB
JavaScript
350 lines
16 KiB
JavaScript
// 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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
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>`;
|
||
}
|