前言
那套 Web 系统,当年立项时 PPT 写得天花乱坠,如今打开后台------日活两位数的,一位是测试,一位是你自己刷新页面。
同事嘴上说「早该重构了」,手上却在问 ChatGPT:「帮我把这个文件夹里的报表下下来。」AI 能聊、能写、能画图,就是进不了你们内网那扇登录页的门------不是 AI 不行,是老系统没给它发工牌。
所谓「赋能」,听起来像董事会词汇,落地就一件事:别让 Agent 重新发明一套登录和下载,而是让它走你们现成的 API。 用户还是在浏览器里扫码登录;Token 进 MCP 进程内存;Agent 说「列一下文件」「下这个附件」,和你们页面点按钮,打的是同一批接口。
本文不讲微服务改造、不上 Kubernetes,只讲怎么用 Node + npm + MCP,给「没人用但还不能关」的老系统,挂一条 AI 能走的旁路。老系统负责活着,AI 负责干活------分工明确,各得其所。
你已经有一套 web 系统:用户登录后就能列文件、下载文件。目标不是改业务栈,而是:
- 用 npm 把 MCP Server 做成可安装、可发布的包;
- 在任意支持 MCP 的 Agent 客户端 里用 npx / node 拉起它;
- Agent 带上登录态,调用和页面相同的 文件列表 / 下载 API。
MCP 是开放协议,不限于 Cursor 。同一 npm 包可接入 Cursor、Claude Desktop、VS Code(MCP 扩展)、Windsurf 等;下文以 mcp.json 为例,各客户端配置路径不同,stdio 启动命令相同。
需要做些什么?
| 步骤 | 做什么 |
|---|---|
| 1 | 确认列表/下载 API;配置 LOGIN_URL、TOKEN_SOURCE |
| 2 | 写 MCP tools:login_with_browser / list_files / download_file |
| 3 | npm install(含 playwright,不 执行 playwright install) |
| 4 | npm publish --registry=https://admin.npm.xxxxer.me/ |
| 5 | 在 Agent 客户端配 mcp.json:npx -y --registry=... files-mcp-client@0.2.0 |
| 6 | Agent 先 login_with_browser(本机 Chrome)→ Token 进内存 → 再列文件/下载 |
一、和现有系统怎么对齐
| 页面能力 | MCP Tool | 典型 API |
|---|---|---|
| 登录 | login_with_browser |
Playwright channel: chrome,凭证存内存 |
| 文件列表 | list_files |
GET /api/files?path= |
| 下载 | download_file |
GET /api/files/download?id= |
| 退出 | logout / clear_auth |
POST /api/logout(可选) |
补充 tools:auth_status、clear_auth、logout(退出见 §3.4)。
鉴权走 login_with_browser 写入的进程内存,不必配 API_TOKEN。内网只在本机访问,不做内网穿透或公网暴露。
二、npm 在这条链路里干什么
| npm 能力 | 用途 |
|---|---|
npm install |
装 sdk、playwright、zod |
package.json + bin |
MCP 可执行入口 |
npm publish |
发到 https://admin.npm.xxxxer.me/ |
npx -y --registry=... 包@版本 |
Agent 客户端拉起 MCP(stdio) |
业务系统继续跑;MCP 是旁路独立包。配置见第四节。
常见 Agent 客户端与配置文件
| 客户端 | 配置文件位置(Windows) |
|---|---|
| Cursor | 项目:<仓库>\.cursor\mcp.json;全局:%USERPROFILE%\.cursor\mcp.json |
| Claude Desktop | %APPDATA%\Claude\claude_desktop_config.json |
| VS Code | 用户/工作区 MCP 配置(扩展提供,结构同为 mcpServers) |
| Windsurf | 同 Cursor,.windsurf/mcp.json 或设置面板 |
各客户端 UI 不同,但 MCP Server 段结构一致 :command + args + env。
三、从零做一个 MCP npm 包
3.1 目录
bash
your-files-mcp/
package.json
src/server.mjs # stdio 入口
.gitignore # 含 .env
可参考:mcp-download-sidecar/。
3.2 package.json
json
{
"name": "files-mcp-client",
"version": "0.2.0",
"type": "module",
"bin": { "files-mcp-client": "./src/server.mjs" },
"files": ["src"],
"publishConfig": { "registry": "https://admin.npm.xxxxer.me/" },
"scripts": { "start": "node src/server.mjs" },
"engines": { "node": ">=18" },
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"playwright": "^1.52.0",
"zod": "^3.25.28"
}
}
playwright 必在 dependencies 里;启动时用 channel: "chrome" 调本机浏览器,不要 playwright install chromium。
3.3 登录与浏览器:Token 获取并写入内存(关键代码)
完整实现见 mcp-download-sidecar/download-server.mjs。核心分四步:
① 进程内会话对象(不落盘)
js
const session = { token: "", cookie: "", obtainedAt: null };
function getToken() {
return session.token; // 后续 list_files / download_file 从这里读
}
② 开本机 Chrome,等用户登录
js
const { chromium } = await import("playwright");
const { context } = await chromium.launchPersistentContext(userDataDir, {
headless: false,
channel: "chrome", // 本机 Chrome,不下载 Chromium
});
const page = context.pages()[0] ?? (await context.newPage());
await page.goto(LOGIN_URL, { waitUntil: "domcontentloaded" });
// 轮询:直到 localStorage / Cookie / 请求头里出现凭证
await waitForLoginCredentials(page, context, { source: TOKEN_SOURCE, timeout });
③ 从页面取出 Token(按 TOKEN_SOURCE 配置)
js
// TOKEN_SOURCE 格式:类型:键名
// 例:cookie:ACCESS_TOKEN | localStorage:token | header:Authorization
async function extractTokenFromPage(page, source) {
const [kind, key] = source.split(":");
if (kind === "localStorage") {
return page.evaluate((k) => localStorage.getItem(k) || "", key);
}
if (kind === "cookie") {
const hit = (await page.context().cookies()).find((c) => c.name === key);
return hit?.value || ""; // key=ACCESS_TOKEN 时取该 Cookie 的值
}
return "";
}
示例:登录后 Cookie 名为 ACCESS_TOKEN
bash
TOKEN_SOURCE=cookie:ACCESS_TOKEN
json
"env": {
"TOKEN_SOURCE": "cookie:ACCESS_TOKEN"
}
登录成功后代码会:
- 从浏览器读到名为
ACCESS_TOKEN的 Cookie 值 → 写入session.token - 同时把所有 Cookie 拼成
session.cookie→ 后续请求带Cookie头 - 若
session.token有值,还会带Authorization: Bearer <ACCESS_TOKEN的值>
若你们后端只认 Cookie: ACCESS_TOKEN=xxx、不认 Bearer,可只依赖 session.cookie(实现里会整串带上)。
js
// 也可拦截页面发出的 Authorization 头(TOKEN_SOURCE=header:Authorization 时用)
let bearer = "";
context.on("request", (req) => {
const m = /^Bearer\s+(.+)$/i.exec(req.headers()["authorization"] || "");
if (m) bearer = m[1];
});
const token = (await extractTokenFromPage(page, TOKEN_SOURCE)) || bearer;
const cookieHeader = (await context.cookies())
.map((c) => `${c.name}=${c.value}`)
.join("; ");
④ 写入内存,供后续 API 使用
js
session.token = token || "";
session.cookie = cookieHeader || "";
session.obtainedAt = new Date().toISOString();
await context.close(); // 关浏览器;凭证留在 session 里
// 之后 list_files / download_file 自动带鉴权头
function buildHeaders() {
const headers = { Accept: "application/json, */*" };
if (session.token) headers.Authorization = `Bearer ${session.token}`;
if (session.cookie) headers.Cookie = session.cookie;
return headers;
}
await fetch(`${API_BASE}/api/files`, { headers: buildHeaders() });
要点:
- 凭证只存在
session对象里,不写文件、不进 mcp.json - MCP 进程重启后
session清空,需重新login_with_browser TOKEN_SOURCE要和你们前端实际存 Token 的方式一致(DevTools → Application 里看 key 名)
常用环境变量:
bash
API_BASE=https://your-app.example.com
LOGIN_URL=https://your-app.example.com/login
TOKEN_SOURCE=cookie:ACCESS_TOKEN # Cookie 名 ACCESS_TOKEN;或 localStorage:token / header:Authorization
PLAYWRIGHT_CHANNEL=chrome
DOWNLOAD_DIR=F:/downloads/app-files
LIST_API_PATH=/api/files
DOWNLOAD_API_PATH=/api/files/download
LOGOUT_API_PATH=/api/logout
3.4 退出登录
退出分三层:
| 层级 | 做什么 | 怎么调 |
|---|---|---|
| MCP 内存 | 清空 session,后续 API 不再带凭证 |
clear_auth |
| 后端会话(推荐) | 服务端 ACCESS_TOKEN 失效 |
logout(配 LOGOUT_API_PATH) |
| 浏览器 Cookie(可选) | 本机 Chrome 里 Cookie 可能还在 | 用户在浏览器点「退出」 |
对 Agent 说「退出登录」 → 调 logout。
json
"env": { "LOGOUT_API_PATH": "/api/logout" }
logout 流程:先带当前 Cookie/Token 调后端 logout(默认 POST)→ 再清空内存(后端失败也会清内存)。
| Tool | 调后端 | 清内存 |
|---|---|---|
clear_auth |
否 | 是 |
logout |
是 | 是 |
验证:auth_status 的 ready 应为 false。流程见图下半部分 「二、退出登录」。
四、发布与接入 Agent 客户端
4.1 publish
bash
npm login --registry=https://admin.npm.xxxxer.me/
npm publish --registry=https://admin.npm.xxxxer.me/
4.2 MCP 配置(通用)
在所用 Agent 的 MCP 配置里加入(Cursor 放 .cursor/mcp.json,Claude Desktop 放 claude_desktop_config.json,其余见上表):
json
{
"mcpServers": {
"files": {
"command": "npx",
"args": [
"-y",
"--registry=https://admin.npm.xxxxer.me/",
"files-mcp-client@0.2.0"
],
"env": {
"API_BASE": "https://你的系统域名",
"LOGIN_URL": "https://你的系统域名/login",
"TOKEN_SOURCE": "cookie:ACCESS_TOKEN",
"PLAYWRIGHT_CHANNEL": "chrome",
"DOWNLOAD_DIR": "F:/downloads/app-files"
}
}
}
}
注意:--registry=... 写成一个 args 项;pin 版本;不要 写 API_TOKEN。
4.3 开发期(未 publish)
command 改为 node,args 指向本地 server.mjs,env 同上。跑通后再切回 npx。
4.4 使用
- 保存配置并重启 Agent 客户端(或 Reload MCP)→ Server 显示已连接
- Agent 调
login_with_browser→ 本机 Chrome 登录 - 调
list_files/download_file - MCP 进程重启后需重新登录
五、边界与验收
做 / 不做
| 做 | 不做 |
|---|---|
| 调已有登录后的 API | 开无鉴权后门 |
| Token 仅存进程内存 | Token 写进 mcp.json / Git |
| 本机访问内网 | 内网穿透、公网暴露 |
验收
- Node ≥ 18;MCP Server 在客户端显示已连接
-
login_with_browser后list_files/download_file成功 - 文件落在
DOWNLOAD_DIR -
npx -y --registry=... files-mcp-client@0.2.0可启动 - 仓库无密钥
六、常见坑
| 现象 | 处理 |
|---|---|
| MCP 未连接 / 红灯 | npx 不在客户端进程的 PATH;开发期用 node 绝对路径 |
npx 拉包失败 |
检查 --registry=https://admin.npm.xxxxer.me/ |
| 401 / 下到登录页 HTML | Token 过期或未登录;重新 login_with_browser |
| 浏览器起不来 | 装本机 Chrome/Edge;不要依赖下载 Chromium |
