从浏览器网络请求快速生成 TypeScript API 与 MCP Server:安全实操指南
本文整理自 ani 在 X 上发布的两帖方法,并补充了凭证清理、最小权限、验证与失败处理。原帖只给出 7 个步骤,没有提供完整代码、测试结果或生产安全方案;下文中的脚本、提示词和验收门槛属于“实践扩展”。
- 原始来源:ani on X
- 原始主题:用 DevTools 网络记录、HAR、Copy as fetch 和 Plan Mode 生成 TypeScript API 与 MCP Server
- 来源边界:短社交媒体线程;可见的两帖已覆盖,但不是完整教程,也没有证明该方法适用于所有网站
先说结论
这套方法适合快速理解网站实际调用的接口,并生成一个只读、可审查的候选实现。它不适合把浏览器会话原样复制进生产代码,因为 HAR 和 Fetch 文本可能包含 Cookie、Authorization、CSRF Token、个人数据和短期签名。
推荐的最小闭环是:
- 在 DevTools 中录制目标操作;
- 导出 HAR 和 Fetch 请求;
- 先清理敏感字段;
- 让编码代理生成只读 API 客户端和 MCP 工具;
- 用测试账号验证认证、分页、错误处理与日志脱敏;
- 写入、删除、发布类操作继续保留人工确认。
适用与不适用场景
适合
- 为没有公开 SDK 的网站制作个人只读查询工具;
- 理解前端如何调用后端接口;
- 为内部系统做受控原型,且你有明确授权;
- 快速生成 API 客户端、类型定义和 MCP 工具草稿。
不适合
- 绕过付费墙、访问控制、验证码或网站反自动化机制;
- 抓取你无权访问的数据;
- 直接复用真实账号 Cookie 或高权限 Token;
- 未经审核就开放写入、删除、发布、付款等工具;
- 假设私有接口长期稳定,或默认网站条款允许自动化调用。
原文方法覆盖清单
| 原文方法 | 是否纳入 | 落点 | 若遗漏原因 |
|---|---|---|---|
| 打开目标网站 | 是 | 步骤 1 | — |
| DevTools → Network → Keep Log | 是 | 步骤 1 | — |
| 退出后重新登录 | 有条件纳入 | 步骤 1 | 只允许测试账号;避免暴露真实高权限会话 |
| 访问所有需要数据的页面 | 是 | 步骤 1、案例清单 | — |
| Copy all as HAR 并保存 | 是 | 步骤 2 | — |
| Copy all as fetch 并保存 | 是 | 步骤 2 | — |
| 放入空 Bun 项目,由 Plan Mode 生成 TypeScript API 和 MCP Server | 是 | 步骤 4 | 增加了只读范围、验证和安全约束 |
准备工作
需要:
- Chromium、Chrome 或 Edge;
- Bun;
- 一个你有权使用的目标网站测试账号;
- 支持读取项目文件并生成 TypeScript 的编码代理;
- Python 3,仅用于离线清理 HAR;脚本只用标准库。
确认 Bun 可用:
bun --version
python3 --version建议先建立隔离目录:
mkdir -p web-api-mcp-candidate/{captures,scripts,src,test}
cd web-api-mcp-candidate
bun init -y项目最初只需要这些文件:
web-api-mcp-candidate/
├── captures/
│ ├── raw.har # 原始文件,不提交版本库
│ ├── sanitized.har # 清理后才允许交给模型
│ └── request.fetch.txt # 清理后才允许交给模型
├── scripts/
│ └── sanitize_har.py
├── src/ # 由编码代理生成候选实现
├── test/ # 由编码代理生成测试
└── .gitignore.gitignore 至少加入:
captures/raw.har
captures/sanitized.har
captures/request.fetch.txt
captures/*.raw.txt
.env
node_modules/步骤 1:录制一个最小、可解释的操作流程
打开目标网站后:
- 打开 DevTools;
- 进入 Network;
- 勾选 Preserve log / Keep Log;
- 清空旧请求;
- 只执行一个明确流程;
- 记录操作与预期结果。
不要一次把整个网站都录进去。每次只录一个业务动作,后面更容易识别真正的接口。
案例 A:公开只读列表
操作:打开产品列表页,再翻到下一页。
需要观察:
- 列表请求 URL;
- 页码或 cursor 参数;
- 返回的数据字段;
- 空列表时的返回结构。
验收:候选客户端能读取第一页、下一页和空页,不需要登录凭证。
案例 B:登录后的只读详情
操作:使用测试账号登录,再打开一条详情记录。
需要观察:
- 登录态来自 Cookie、Bearer Token 还是其他机制;
- Token 是否过期;
- 401/403 时网站如何刷新会话;
- 响应中是否包含个人数据。
验收:凭证只能从环境变量或运行时会话注入;源码、日志和测试快照中不得出现真实凭证。
案例 C:写入操作
操作:仅在测试环境观察“创建草稿”请求,不提交真实业务数据。
验收:第一版 MCP 工具不直接执行写入,而是输出待确认的请求摘要;真正调用必须经过人工批准。
步骤 2:导出 HAR 与 Fetch 请求
在 Network 面板中:
- 右键请求列表,选择 Copy all as HAR 或 Save all as HAR with content;
- 保存为
captures/raw.har; - 对关键请求选择 Copy as fetch;
- 保存为
captures/request.fetch.txt。
此时不要提交 Git,也不要把文件直接交给模型。 先假设其中包含凭证。
步骤 3:离线清理 HAR
文件:scripts/sanitize_har.py
#!/usr/bin/env python3
from __future__ import annotations
import json
import sys
from pathlib import Path
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
SENSITIVE_HEADERS = {
"authorization",
"cookie",
"proxy-authorization",
"set-cookie",
"x-api-key",
"x-csrf-token",
"x-xsrf-token",
}
SENSITIVE_QUERY = {
"access_token",
"api_key",
"auth",
"code",
"key",
"password",
"signature",
"token",
}
def sanitize_url(url: str) -> str:
parts = urlsplit(url)
query = [
(key, "[REDACTED]" if key.lower() in SENSITIVE_QUERY else value)
for key, value in parse_qsl(parts.query, keep_blank_values=True)
]
return urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment))
def sanitize_headers(headers: list[dict[str, str]]) -> list[dict[str, str]]:
return [
{**header, "value": "[REDACTED]"}
if header.get("name", "").lower() in SENSITIVE_HEADERS
else header
for header in headers
]
def sanitize_har(data: dict) -> dict:
for entry in data.get("log", {}).get("entries", []):
request = entry.get("request", {})
response = entry.get("response", {})
request["url"] = sanitize_url(request.get("url", ""))
request["headers"] = sanitize_headers(request.get("headers", []))
request["cookies"] = []
request["queryString"] = [
{**item, "value": "[REDACTED]"}
if item.get("name", "").lower() in SENSITIVE_QUERY
else item
for item in request.get("queryString", [])
]
if "postData" in request:
request["postData"]["text"] = "[REDACTED — review manually]"
response["headers"] = sanitize_headers(response.get("headers", []))
response["cookies"] = []
return data
def self_test() -> None:
sample = {
"log": {
"entries": [
{
"request": {
"url": "https://example.test/items?token=secret&page=2",
"headers": [{"name": "Authorization", "value": "Bearer secret"}],
"cookies": [{"name": "session", "value": "secret"}],
"queryString": [
{"name": "token", "value": "secret"},
{"name": "page", "value": "2"},
],
"postData": {"text": "private body"},
},
"response": {
"headers": [{"name": "Set-Cookie", "value": "session=secret"}],
"cookies": [{"name": "session", "value": "secret"}],
},
}
]
}
}
result = sanitize_har(sample)
entry = result["log"]["entries"][0]
rendered = json.dumps(result)
assert "secret" not in rendered
assert "page=2" in entry["request"]["url"]
assert entry["request"]["postData"]["text"].startswith("[REDACTED")
print("self-test: ok")
def main() -> None:
if sys.argv[1:] == ["--self-test"]:
self_test()
return
if len(sys.argv) != 3:
raise SystemExit("usage: sanitize_har.py INPUT.har OUTPUT.har")
source, destination = map(Path, sys.argv[1:])
data = json.loads(source.read_text(encoding="utf-8"))
cleaned = sanitize_har(data)
destination.write_text(json.dumps(cleaned, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"wrote: {destination}")
if __name__ == "__main__":
main()先运行自检,再处理真实 HAR:
python3 scripts/sanitize_har.py --self-test
python3 scripts/sanitize_har.py captures/raw.har captures/sanitized.har预期输出:
self-test: ok
wrote: captures/sanitized.har再做一次关键词扫描:
python3 -c 'import json; from pathlib import Path; d=json.loads(Path("captures/sanitized.har").read_text()); sensitive={"authorization","cookie","proxy-authorization","set-cookie","x-api-key","x-csrf-token","x-xsrf-token"}; parts=(part for e in d.get("log",{}).get("entries",[]) for part in (e.get("request",{}),e.get("response",{}))); assert all(not part.get("cookies") and all(h.get("value")=="[REDACTED]" for h in part.get("headers",[]) if h.get("name","").lower() in sensitive) for part in parts); print("basic secret scan: ok")'这只是基础清理,不是完整的数据防泄漏工具。仍需人工检查:
- 用户名、邮箱、手机号和地址;
- 响应正文中的业务数据;
- 自定义认证头;
- URL 路径中的用户 ID 或临时签名;
- Fetch 文本中的
credentials、Cookie 和 Token。
Copy as fetch 文件建议人工保留 URL、方法和普通请求头,删除所有凭证值和真实请求体。若无法确认某字段是否敏感,直接删掉。
步骤 4:让编码代理生成候选 API 和 MCP Server
将 captures/sanitized.har 和清理后的 captures/request.fetch.txt 放在空 Bun 项目中,然后在 Plan Mode 使用以下提示词:
读取 captures/sanitized.har 和 captures/request.fetch.txt,先不要写代码。
目标:生成一个最小 TypeScript API 客户端和 MCP Server 候选实现。
边界:
1. 第一版只允许只读接口;不要实现创建、修改、删除、发布、付款等操作。
2. 不得把 Cookie、Token、CSRF 值或其他凭证写入源码、测试、日志或示例。
3. 认证信息只能从环境变量或调用方注入。
4. 从抓包中识别 base URL、请求方法、参数、分页方式、成功响应和错误响应。
5. 对来源证据不足的字段标记 TODO,不要猜测。
6. MCP 工具使用明确、互不重叠的名称和输入 schema。
7. 为每个客户端方法生成 mock 测试,至少覆盖成功、空结果、401/403、429 和 5xx。
8. 所有网络调用设置超时;只对安全的只读请求做有上限的重试。
先输出:
- 接口清单;
- 认证与敏感数据风险;
- 拟生成文件树;
- 缺失信息;
- 验收计划。
我确认计划后再生成代码。计划确认后,要求生成的最小结构可以是:
src/
├── client.ts # HTTP 客户端、超时、错误映射
├── schemas.ts # 输入与响应类型
├── tools.ts # 只读 MCP 工具定义
└── server.ts # MCP Server 入口
test/
├── client.test.ts
└── tools.test.ts不要为了“架构完整”增加单实现工厂、插件系统或多层注册器。一个客户端、几个只读工具和对应测试足够验证方法。
步骤 5:验证生成结果
1. 静态检查
bun install
bun run typecheck
bun test如果代理没有生成 typecheck 脚本,先让它补充到 package.json,不要跳过类型检查。
2. 凭证检查
python3 -c 'from pathlib import Path; files=list(Path("src").rglob("*.ts"))+list(Path("test").rglob("*.ts")); text="\n".join(p.read_text() for p in files); assert "Bearer ey" not in text and "session=" not in text; print("source secret scan: ok")'3. Mock 验收
至少验证:
| 场景 | 预期行为 |
|---|---|
| 正常列表 | 返回经过类型校验的数据 |
| 空列表 | 返回空数组,不抛出伪错误 |
| 401/403 | 返回明确的认证/权限错误,不打印凭证 |
| 429 | 尊重服务端提示;有上限重试或直接返回限流错误 |
| 5xx/超时 | 返回带上下文的错误,不无限重试 |
| 分页 | 不重复、不漏项,并有终止条件 |
4. 测试账号烟雾验证
只有 Mock 通过后,才使用低权限测试账号:
- 从环境变量注入测试凭证;
- 只调用一个只读工具;
- 检查请求范围和返回字段;
- 检查日志没有凭证与个人数据;
- 删除本地测试凭证并确认未进入 Git。
第一轮不要测试写入工具。
步骤 6:为 MCP 工具设置安全边界
只读工具也应明确:
- 工具名和用途;
- 输入 schema;
- 允许访问的 host;
- 超时、最大页数和最大返回条数;
- 日志中允许记录的字段;
- 错误返回格式。
写入类能力如果以后确实需要,再单独增加:
dry_run=true默认值;- 待执行操作摘要;
- 人工确认;
- 幂等键;
- 审计记录;
- 回滚或补偿方案。
步骤 7:选择企业内网部署模式
企业内网不要默认共享一组浏览器 Cookie。先按上游系统的认证方式选择部署模式:
| 上游系统情况 | 推荐模式 | 原因 |
|---|---|---|
| 数据公开或所有员工共享、只读 | 共享 Streamable HTTP MCP | 容易集中运维、审计和限流 |
| 上游支持员工身份或 OAuth On-Behalf-Of | 共享 Streamable HTTP MCP,并下推用户身份 | 保留调用者权限,避免共享超级账号 |
| 只能依赖个人浏览器 Cookie | 每用户本地 stdio MCP | Cookie 留在用户设备,不进入中心服务 |
| 需要写入、删除、发布等高风险操作 | 暂不共享;先做只读服务 | 等审批、幂等、审计和回滚闭环完成后再加 |
推荐的企业内网最小架构是:
员工的 AI/MCP Client
│ HTTPS + 企业访问令牌
▼
内网 API Gateway / SSO
- TLS
- OIDC/JWT 校验
- 清除外部身份头
- 转发原始 Bearer Token 供 MCP 二次验签
- 限流、审计、请求大小限制
│ 仅内网或同机回环访问
▼
只读 MCP Server(Streamable HTTP,/mcp)
│ 用户身份下推 / 受限服务账号
▼
目标网站或企业内部 APIMCP TypeScript SDK 的官方建议是:远程、多客户端连接使用 Streamable HTTP;由宿主进程启动的本地服务使用 stdio。共享服务必须在 MCP handler 前验证 Host、Origin 和访问令牌;createMcpHandler 本身不会替你完成这些检查。
三条部署红线
- 不要把
raw.har、浏览器 Cookie 或抓包 Token 打进镜像; - 不要让 MCP 容器直接暴露到办公网,更不要暴露到公网;
- 不要使用一个共享超级账号代表所有员工访问上游系统。
步骤 8:增加企业内网 HTTP 入口
以下是实践扩展:假设步骤 4 生成的 src/server.ts 已被整理为可复用的 buildServer(authInfo) 工厂。不要再复制一套工具定义;stdio 和 HTTP 两种入口复用同一个工厂。
调整后的文件结构:
src/
├── client.ts
├── schemas.ts
├── tools.ts
├── server-factory.ts # 唯一的 MCP 工具注册入口
├── server-stdio.ts # 个人本地模式
└── server-http.ts # 企业共享模式
Dockerfile
compose.yaml安装官方 SDK 依赖;版本应由企业制品库或 lockfile 固定:
bun add @modelcontextprotocol/server jose zod
bun add -d typescript @types/bunHTTP 入口
文件:src/server-http.ts
import {
createMcpHandler,
OAuthError,
OAuthErrorCode,
requireBearerAuth,
type AuthInfo,
} from "@modelcontextprotocol/server";
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose";
import { buildServer } from "./server-factory";
function requiredEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`missing required environment variable: ${name}`);
return value;
}
const issuer = requiredEnv("OIDC_ISSUER");
const audience = requiredEnv("OIDC_AUDIENCE");
const jwks = createRemoteJWKSet(new URL(requiredEnv("OIDC_JWKS_URL")));
const allowedHosts = new Set(
requiredEnv("ALLOWED_HOSTS").split(",").map((value) => value.trim()).filter(Boolean),
);
const allowedOrigins = new Set(
requiredEnv("ALLOWED_ORIGINS").split(",").map((value) => value.trim()).filter(Boolean),
);
function scopesFrom(payload: JWTPayload): string[] {
if (typeof payload.scope === "string") return payload.scope.split(" ").filter(Boolean);
const scp = payload.scp;
return Array.isArray(scp)
? scp.filter((value): value is string => typeof value === "string")
: [];
}
async function verifyAccessToken(token: string): Promise<AuthInfo> {
try {
const { payload } = await jwtVerify(token, jwks, { issuer, audience });
if (!payload.sub || typeof payload.exp !== "number") throw new Error("missing sub/exp");
return {
token,
clientId: payload.sub,
scopes: scopesFrom(payload),
expiresAt: payload.exp,
};
} catch {
throw new OAuthError(OAuthErrorCode.InvalidToken, "invalid or expired access token");
}
}
const gate = requireBearerAuth({
verifier: { verifyAccessToken },
requiredScopes: ["mcp"],
});
const handler = createMcpHandler(
({ authInfo }) => buildServer(authInfo),
{ responseMode: "json", legacy: "reject" },
);
function forbidden(message: string): Response {
return Response.json({ error: message }, { status: 403 });
}
export default {
hostname: "0.0.0.0",
port: Number(process.env.PORT ?? "3000"),
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/healthz") {
return Response.json({ status: "ok" });
}
if (url.pathname !== "/mcp") {
return new Response("not found", { status: 404 });
}
if (!allowedHosts.has(url.hostname)) {
return forbidden("host not allowed");
}
const origin = request.headers.get("origin");
if (origin && !allowedOrigins.has(origin)) {
return forbidden("origin not allowed");
}
const auth = await gate(request);
if (auth instanceof Response) return auth;
return handler.fetch(request, { authInfo: auth });
},
};
process.on("SIGTERM", async () => {
await handler.close();
process.exit(0);
});这里采用“企业网关先做接入控制,MCP 服务再次验证 Bearer JWT”的默认模式:
- 网关负责 TLS、网络入口、基础限流和粗粒度访问策略;
- MCP 服务使用企业 IdP 的 JWKS 验证签名、issuer、audience、有效期和
mcpscope; clientId与scopes只来自已验签 Token,不信任客户端或网关注入的普通身份头;- MCP 服务继续按工具检查 scope,并把
clientId用于上游权限下推和审计; - 上游系统需要用户权限时,使用企业 IdP 支持的 On-Behalf-Of / Token Exchange;不能直接把当前 MCP Token 当作上游 Token,除非上游 audience 明确匹配。
如果企业 IdP 只支持 opaque Token,把本地 JWKS 验证替换为 RFC 7662 introspection;仍需校验 active、audience、scope、subject 与过期时间。
工具级授权
文件:src/server-factory.ts
import { McpServer, type AuthInfo } from "@modelcontextprotocol/server";
import { z } from "zod/v4";
import { ApiClient } from "./client";
export function buildServer(authInfo?: AuthInfo): McpServer {
const server = new McpServer({ name: "internal-web-api", version: "1.0.0" });
const client = new ApiClient({ callerId: authInfo?.clientId });
server.registerTool(
"list-items",
{
description: "读取当前用户有权查看的条目;最多返回 100 条",
inputSchema: z.object({
cursor: z.string().optional(),
limit: z.number().int().min(1).max(100).default(20),
}),
},
async ({ cursor, limit }) => {
if (!authInfo?.scopes.includes("mcp")) {
return {
content: [{ type: "text", text: "insufficient_scope: requires mcp" }],
isError: true,
};
}
const result = await client.listItems({ cursor, limit });
return {
content: [{ type: "text", text: JSON.stringify(result) }],
};
},
);
return server;
}ApiClient 和 listItems 的具体字段来自步骤 4 的抓包结果;上面代码定义的是部署与授权接口,不应凭空替换真实上游协议。callerId 必须参与上游权限下推或审计,不能只写进日志就宣称实现了用户隔离。
步骤 9:容器化并部署在网关后面
文件:Dockerfile
ARG BUN_IMAGE=oven/bun:1
FROM ${BUN_IMAGE}
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile --production
COPY src ./src
USER bun
EXPOSE 3000
CMD ["bun", "run", "src/server-http.ts"]oven/bun:1 只是教程默认值。企业部署应把 BUN_IMAGE 固定到内部镜像仓库的版本或 digest,并经过镜像扫描。
文件:compose.yaml
services:
mcp:
build:
context: .
args:
BUN_IMAGE: ${BUN_IMAGE:-oven/bun:1}
restart: unless-stopped
read_only: true
tmpfs:
- /tmp:size=64m,noexec,nosuid
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
ports:
- "127.0.0.1:3000:3000"
environment:
PORT: "3000"
ALLOWED_HOSTS: "mcp.intra.example.com"
ALLOWED_ORIGINS: "https://ai.intra.example.com"
OIDC_ISSUER: "https://idp.intra.example.com/"
OIDC_AUDIENCE: "https://mcp.intra.example.com/mcp"
OIDC_JWKS_URL: "https://idp.intra.example.com/.well-known/jwks.json"
healthcheck:
test:
- CMD
- bun
- -e
- "fetch('http://127.0.0.1:3000/healthz').then(r => { if (r.status !== 200) process.exit(1) }).catch(() => process.exit(1))"
interval: 30s
timeout: 5s
retries: 3启动并检查:
BUN_IMAGE=registry.intra.example.com/base/bun@sha256:REPLACE_ME \
docker compose up -d --build
docker compose ps
curl --fail http://127.0.0.1:3000/healthz预期:容器状态为 healthy,健康检查返回:
{"status":"ok"}企业网关必须实现的合同
网关产品可以是现有 API Gateway、Ingress、Service Mesh 或带 OIDC 的反向代理,不必为了 MCP 再引入一套网关。最小合同是:
- 仅将
/mcp转发到127.0.0.1:3000或私有 Pod 地址; - 对客户端执行 OIDC/JWT 接入校验,拒绝明显无效流量;
- 删除客户端提交的
X-Authenticated-User、X-Authenticated-Scopes等可伪造身份头,不再向 MCP 注入普通身份头; - 将原始 Bearer Token 转发给 MCP 服务做第二次签名、issuer、audience、有效期和 scope 验证;
Authorization必须在网关、MCP 和 APM 日志中脱敏,且不得转发给错误 audience 的上游系统;- 限制请求体、连接时长、并发数和调用频率;
- 禁止缓存
/mcp响应; - 记录调用者、工具名、状态、耗时、审批结果和关联 ID,但不记录凭证及完整敏感结果。
即使客户端绕过网关抵达 MCP 端口,也必须因为缺少有效的目标 audience Token 而被 MCP 服务拒绝;网络隔离仍然保留为第二道边界。
步骤 10:连接和使用 MCP
开发验收:MCP Inspector
MCP Inspector 是官方测试客户端。受保护的内网端点可以通过企业访问令牌连接:
export MCP_TOKEN='由企业 IdP 或开发者门户签发的短期令牌'
npx @modelcontextprotocol/inspector \
--server-url https://mcp.intra.example.com/mcp \
--transport http \
--header "Authorization: Bearer $MCP_TOKEN"列出工具:
npx @modelcontextprotocol/inspector --cli \
https://mcp.intra.example.com/mcp \
--transport http \
--header "Authorization: Bearer $MCP_TOKEN" \
--method tools/list调用只读工具:
npx @modelcontextprotocol/inspector --cli \
https://mcp.intra.example.com/mcp \
--transport http \
--header "Authorization: Bearer $MCP_TOKEN" \
--method tools/call \
--tool-name list-items \
--tool-args-json '{"limit":5}' \
--format json预期结果不是固定业务数据,而是:
- 只返回当前员工有权查看的数据;
- 数量不超过
limit和服务端上限; - 返回中没有 Cookie、Token 或不必要的个人数据;
- 审计日志能关联到当前
clientId; - 无 scope 时返回
insufficient_scope,而不是继续调用上游。
接入企业 AI 客户端
不同 MCP Host 的配置字段不同,不要复制未经验证的固定 JSON。需要提供给客户端管理员的合同只有:
- Transport:
Streamable HTTP; - URL:
https://mcp.intra.example.com/mcp; - Authentication:企业 OAuth/OIDC 或短期 Bearer Token;
- Scope:至少
mcp,高风险工具使用单独 scope; - CA:企业内部 CA 必须进入客户端信任库;禁止关闭 TLS 校验;
- 数据策略:客户端不得把 MCP 返回结果发送到未批准的外部模型。
接入后先用三条低风险提示验证:
列出我有权查看的 5 条记录,只返回名称和更新时间。查询记录 ID demo-123;如果无权访问,原样报告权限错误,不要尝试其他账号。告诉我当前可用的工具及其只读/写入属性,不执行任何工具。个人 Cookie 场景:改用 stdio
如果上游网站只能使用个人浏览器 Cookie,中心化 Streamable HTTP 方案不成立。此时让每个用户在本机运行 stdio MCP,并由本地 MCP Host 启动:
MCP Host → stdio → 用户本机 server-stdio.ts → 用户有权访问的上游系统Cookie 放在操作系统凭证库或受限本地会话中,不放入 MCP 配置文件、命令行参数、Git 或中心服务器。只有上游提供可安全委派的 OAuth/OBO 机制后,再迁移到共享 HTTP 服务。
步骤 11:企业上线检查
网络与认证
- MCP 容器无法被员工终端直接访问,只能经企业网关;
- 网关校验 issuer、audience、签名、有效期和 scope;
- 客户端伪造的身份头会被删除;
- TLS 使用企业信任链,客户端未关闭证书验证;
- 上游权限按用户下推,或明确限定为非个人共享只读服务账号。
数据与工具
- 第一阶段只有只读工具;
- 每个工具有最大条数、最大页数、超时和并发限制;
- 敏感字段在服务端裁剪,而不是只靠模型“不要展示”;
- MCP 返回数据只进入企业批准的模型和日志系统;
- 原始 HAR、Cookie 和抓包 Token 未进入镜像、制品库或日志。
运维与审计
/healthz不访问上游敏感数据;- 监控请求量、P95/P99 延迟、401/403、429、5xx 和上游超时;
- 审计包含调用者、工具名、参数摘要、状态、耗时和关联 ID;
- 日志不保存 Authorization、Cookie 和完整敏感响应;
- 有一键回滚到上一镜像的方法;
- 上游接口变化会触发测试失败,而不是静默返回空数据。
常见失败与处理
HAR 太大,代理无法理解
只保留一个业务流程对应的请求;删除图片、字体、分析脚本和重复轮询。不要先建复杂解析器,缩小输入通常更有效。
生成代码只能复现一次成功请求
要求补充认证续期、分页、空结果、429、5xx 和超时测试。一次成功回放不等于可维护 API。
Token 很快过期
不要固化 Token。先识别合法的认证续期方式;无法确认时,让用户每次显式提供短期凭证,或停止该集成。
网站接口变化
把响应解析集中在少量 schema/mapper 中,并让失败明确暴露。不要静默吞掉未知字段或把空数据当成功。
MCP 工具选择混乱
缩短并区分工具说明,避免多个工具使用近似描述;优先保留更少、更明确的工具。
涉及网站条款或访问权限
停止自动化,先确认你有权访问、保存和处理目标数据。技术上能调用不代表被授权调用。
最终验收清单
- 原始 HAR 未提交 Git,也未发送给模型;
- HAR 和 Fetch 文本已清理 Cookie、Token、CSRF 和个人数据;
- 第一版只包含只读工具;
- 凭证只从环境变量或运行时注入;
- 成功、空结果、401/403、429、5xx、超时和分页均有测试;
- 网络请求有超时,重试次数有上限;
- 日志不包含凭证与个人数据;
- MCP 工具权限、host、输入 schema 和返回上限明确;
- 测试账号烟雾验证通过;
- 已确认目标网站条款和数据权限;
- 写入类操作未开放,或具备 dry-run、人工确认、审计与回滚。
官方参考
方法边界
原文事实: 原帖建议用 DevTools Keep Log 记录登录与页面访问,导出 HAR 和 Fetch 请求,再放入空 Bun 项目,让 Plan Mode 生成 TypeScript API 与 MCP Server。
本文提炼: 将原帖步骤整理为“录制 → 清理 → 计划 → 生成 → Mock 验证 → 测试账号烟雾验证”的最小闭环。
实践扩展: HAR 清理脚本、三类案例、提示词、安全边界、测试矩阵、Streamable HTTP/stdio 部署选择、企业网关合同、容器示例和验收清单均为本文补充,不是原帖声称已经验证的内容。