# 从浏览器网络请求快速生成 TypeScript API 与 MCP Server：安全实操指南

> 本文整理自 ani 在 X 上发布的两帖方法，并补充了凭证清理、最小权限、验证与失败处理。原帖只给出 7 个步骤，没有提供完整代码、测试结果或生产安全方案；下文中的脚本、提示词和验收门槛属于“实践扩展”。

- 原始来源：[ani on X](https://x.com/anaisbetts/status/2084258524448665704)
- 原始主题：用 DevTools 网络记录、HAR、Copy as fetch 和 Plan Mode 生成 TypeScript API 与 MCP Server
- 来源边界：短社交媒体线程；可见的两帖已覆盖，但不是完整教程，也没有证明该方法适用于所有网站

## 先说结论

这套方法适合快速理解网站实际调用的接口，并生成一个**只读、可审查的候选实现**。它不适合把浏览器会话原样复制进生产代码，因为 HAR 和 Fetch 文本可能包含 Cookie、Authorization、CSRF Token、个人数据和短期签名。

推荐的最小闭环是：

1. 在 DevTools 中录制目标操作；
2. 导出 HAR 和 Fetch 请求；
3. 先清理敏感字段；
4. 让编码代理生成只读 API 客户端和 MCP 工具；
5. 用测试账号验证认证、分页、错误处理与日志脱敏；
6. 写入、删除、发布类操作继续保留人工确认。

## 适用与不适用场景

### 适合

- 为没有公开 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 可用：

```bash
bun --version
python3 --version
```

建议先建立隔离目录：

```bash
mkdir -p web-api-mcp-candidate/{captures,scripts,src,test}
cd web-api-mcp-candidate
bun init -y
```

项目最初只需要这些文件：

```text
web-api-mcp-candidate/
├── captures/
│   ├── raw.har              # 原始文件，不提交版本库
│   ├── sanitized.har        # 清理后才允许交给模型
│   └── request.fetch.txt    # 清理后才允许交给模型
├── scripts/
│   └── sanitize_har.py
├── src/                     # 由编码代理生成候选实现
├── test/                    # 由编码代理生成测试
└── .gitignore
```

`.gitignore` 至少加入：

```gitignore
captures/raw.har
captures/sanitized.har
captures/request.fetch.txt
captures/*.raw.txt
.env
node_modules/
```

## 步骤 1：录制一个最小、可解释的操作流程

打开目标网站后：

1. 打开 DevTools；
2. 进入 **Network**；
3. 勾选 **Preserve log / Keep Log**；
4. 清空旧请求；
5. 只执行一个明确流程；
6. 记录操作与预期结果。

不要一次把整个网站都录进去。每次只录一个业务动作，后面更容易识别真正的接口。

### 案例 A：公开只读列表

操作：打开产品列表页，再翻到下一页。

需要观察：

- 列表请求 URL；
- 页码或 cursor 参数；
- 返回的数据字段；
- 空列表时的返回结构。

验收：候选客户端能读取第一页、下一页和空页，不需要登录凭证。

### 案例 B：登录后的只读详情

操作：使用**测试账号**登录，再打开一条详情记录。

需要观察：

- 登录态来自 Cookie、Bearer Token 还是其他机制；
- Token 是否过期；
- 401/403 时网站如何刷新会话；
- 响应中是否包含个人数据。

验收：凭证只能从环境变量或运行时会话注入；源码、日志和测试快照中不得出现真实凭证。

### 案例 C：写入操作

操作：仅在测试环境观察“创建草稿”请求，不提交真实业务数据。

验收：第一版 MCP 工具不直接执行写入，而是输出待确认的请求摘要；真正调用必须经过人工批准。

## 步骤 2：导出 HAR 与 Fetch 请求

在 Network 面板中：

1. 右键请求列表，选择 **Copy all as HAR** 或 **Save all as HAR with content**；
2. 保存为 `captures/raw.har`；
3. 对关键请求选择 **Copy as fetch**；
4. 保存为 `captures/request.fetch.txt`。

**此时不要提交 Git，也不要把文件直接交给模型。** 先假设其中包含凭证。

## 步骤 3：离线清理 HAR

文件：`scripts/sanitize_har.py`

```python
#!/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：

```bash
python3 scripts/sanitize_har.py --self-test
python3 scripts/sanitize_har.py captures/raw.har captures/sanitized.har
```

预期输出：

```text
self-test: ok
wrote: captures/sanitized.har
```

再做一次关键词扫描：

```bash
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 使用以下提示词：

```text
读取 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. 所有网络调用设置超时；只对安全的只读请求做有上限的重试。

先输出：
- 接口清单；
- 认证与敏感数据风险；
- 拟生成文件树；
- 缺失信息；
- 验收计划。

我确认计划后再生成代码。
```

计划确认后，要求生成的最小结构可以是：

```text
src/
├── client.ts       # HTTP 客户端、超时、错误映射
├── schemas.ts      # 输入与响应类型
├── tools.ts        # 只读 MCP 工具定义
└── server.ts       # MCP Server 入口
test/
├── client.test.ts
└── tools.test.ts
```

不要为了“架构完整”增加单实现工厂、插件系统或多层注册器。一个客户端、几个只读工具和对应测试足够验证方法。

## 步骤 5：验证生成结果

### 1. 静态检查

```bash
bun install
bun run typecheck
bun test
```

如果代理没有生成 `typecheck` 脚本，先让它补充到 `package.json`，不要跳过类型检查。

### 2. 凭证检查

```bash
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 通过后，才使用低权限测试账号：

1. 从环境变量注入测试凭证；
2. 只调用一个只读工具；
3. 检查请求范围和返回字段；
4. 检查日志没有凭证与个人数据；
5. 删除本地测试凭证并确认未进入 Git。

第一轮不要测试写入工具。

## 步骤 6：为 MCP 工具设置安全边界

只读工具也应明确：

- 工具名和用途；
- 输入 schema；
- 允许访问的 host；
- 超时、最大页数和最大返回条数；
- 日志中允许记录的字段；
- 错误返回格式。

写入类能力如果以后确实需要，再单独增加：

- `dry_run=true` 默认值；
- 待执行操作摘要；
- 人工确认；
- 幂等键；
- 审计记录；
- 回滚或补偿方案。

## 步骤 7：选择企业内网部署模式

企业内网不要默认共享一组浏览器 Cookie。先按上游系统的认证方式选择部署模式：

| 上游系统情况 | 推荐模式 | 原因 |
| :--- | :--- | :--- |
| 数据公开或所有员工共享、只读 | 共享 Streamable HTTP MCP | 容易集中运维、审计和限流 |
| 上游支持员工身份或 OAuth On-Behalf-Of | 共享 Streamable HTTP MCP，并下推用户身份 | 保留调用者权限，避免共享超级账号 |
| 只能依赖个人浏览器 Cookie | 每用户本地 stdio MCP | Cookie 留在用户设备，不进入中心服务 |
| 需要写入、删除、发布等高风险操作 | 暂不共享；先做只读服务 | 等审批、幂等、审计和回滚闭环完成后再加 |

推荐的企业内网最小架构是：

```text
员工的 AI/MCP Client
        │ HTTPS + 企业访问令牌
        ▼
内网 API Gateway / SSO
  - TLS
  - OIDC/JWT 校验
  - 清除外部身份头
  - 转发原始 Bearer Token 供 MCP 二次验签
  - 限流、审计、请求大小限制
        │ 仅内网或同机回环访问
        ▼
只读 MCP Server（Streamable HTTP，/mcp）
        │ 用户身份下推 / 受限服务账号
        ▼
目标网站或企业内部 API
```

MCP TypeScript SDK 的官方建议是：远程、多客户端连接使用 **Streamable HTTP**；由宿主进程启动的本地服务使用 **stdio**。共享服务必须在 MCP handler 前验证 Host、Origin 和访问令牌；`createMcpHandler` 本身不会替你完成这些检查。

### 三条部署红线

1. 不要把 `raw.har`、浏览器 Cookie 或抓包 Token 打进镜像；
2. 不要让 MCP 容器直接暴露到办公网，更不要暴露到公网；
3. 不要使用一个共享超级账号代表所有员工访问上游系统。

## 步骤 8：增加企业内网 HTTP 入口

以下是**实践扩展**：假设步骤 4 生成的 `src/server.ts` 已被整理为可复用的 `buildServer(authInfo)` 工厂。不要再复制一套工具定义；stdio 和 HTTP 两种入口复用同一个工厂。

调整后的文件结构：

```text
src/
├── client.ts
├── schemas.ts
├── tools.ts
├── server-factory.ts   # 唯一的 MCP 工具注册入口
├── server-stdio.ts     # 个人本地模式
└── server-http.ts      # 企业共享模式
Dockerfile
compose.yaml
```

安装官方 SDK 依赖；版本应由企业制品库或 lockfile 固定：

```bash
bun add @modelcontextprotocol/server jose zod
bun add -d typescript @types/bun
```

### HTTP 入口

文件：`src/server-http.ts`

```typescript
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、有效期和 `mcp` scope；
- `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`

```typescript
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`

```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`

```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
```

启动并检查：

```bash
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`，健康检查返回：

```json
{"status":"ok"}
```

### 企业网关必须实现的合同

网关产品可以是现有 API Gateway、Ingress、Service Mesh 或带 OIDC 的反向代理，不必为了 MCP 再引入一套网关。最小合同是：

1. 仅将 `/mcp` 转发到 `127.0.0.1:3000` 或私有 Pod 地址；
2. 对客户端执行 OIDC/JWT 接入校验，拒绝明显无效流量；
3. 删除客户端提交的 `X-Authenticated-User`、`X-Authenticated-Scopes` 等可伪造身份头，不再向 MCP 注入普通身份头；
4. 将原始 Bearer Token 转发给 MCP 服务做第二次签名、issuer、audience、有效期和 scope 验证；
5. `Authorization` 必须在网关、MCP 和 APM 日志中脱敏，且不得转发给错误 audience 的上游系统；
6. 限制请求体、连接时长、并发数和调用频率；
7. 禁止缓存 `/mcp` 响应；
8. 记录调用者、工具名、状态、耗时、审批结果和关联 ID，但不记录凭证及完整敏感结果。

即使客户端绕过网关抵达 MCP 端口，也必须因为缺少有效的目标 audience Token 而被 MCP 服务拒绝；网络隔离仍然保留为第二道边界。

## 步骤 10：连接和使用 MCP

### 开发验收：MCP Inspector

MCP Inspector 是官方测试客户端。受保护的内网端点可以通过企业访问令牌连接：

```bash
export MCP_TOKEN='由企业 IdP 或开发者门户签发的短期令牌'
npx @modelcontextprotocol/inspector \
  --server-url https://mcp.intra.example.com/mcp \
  --transport http \
  --header "Authorization: Bearer $MCP_TOKEN"
```

列出工具：

```bash
npx @modelcontextprotocol/inspector --cli \
  https://mcp.intra.example.com/mcp \
  --transport http \
  --header "Authorization: Bearer $MCP_TOKEN" \
  --method tools/list
```

调用只读工具：

```bash
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 返回结果发送到未批准的外部模型。

接入后先用三条低风险提示验证：

```text
列出我有权查看的 5 条记录，只返回名称和更新时间。
```

```text
查询记录 ID demo-123；如果无权访问，原样报告权限错误，不要尝试其他账号。
```

```text
告诉我当前可用的工具及其只读/写入属性，不执行任何工具。
```

### 个人 Cookie 场景：改用 stdio

如果上游网站只能使用个人浏览器 Cookie，中心化 Streamable HTTP 方案不成立。此时让每个用户在本机运行 stdio MCP，并由本地 MCP Host 启动：

```text
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、人工确认、审计与回滚。

## 官方参考

- [MCP TypeScript SDK：Serve over HTTP](https://ts.sdk.modelcontextprotocol.io/v2/serving/http.html)
- [MCP TypeScript SDK：Require authorization](https://ts.sdk.modelcontextprotocol.io/v2/serving/authorization.html)
- [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector)

## 方法边界

**原文事实：** 原帖建议用 DevTools Keep Log 记录登录与页面访问，导出 HAR 和 Fetch 请求，再放入空 Bun 项目，让 Plan Mode 生成 TypeScript API 与 MCP Server。

**本文提炼：** 将原帖步骤整理为“录制 → 清理 → 计划 → 生成 → Mock 验证 → 测试账号烟雾验证”的最小闭环。

**实践扩展：** HAR 清理脚本、三类案例、提示词、安全边界、测试矩阵、Streamable HTTP/stdio 部署选择、企业网关合同、容器示例和验收清单均为本文补充，不是原帖声称已经验证的内容。
