MCP 面试高频考点:如何用 TypeScript SDK 设计带人工确认的高风险 Tool?
面试场景:某本地 AI 办公助手通过 stdio 启动你实现的 MCP Server,暴露一个“批量删除指定客户资料”的 Tool。这是不可逆、影响面大的高风险操作。面试官希望你从能力建模、传输、权限、确认流程到审计日志,给出一套可落地的设计,并解释边界与取舍。
一、基础问题:先把能力边界说清楚
面试官:如果让你用 TypeScript MCP SDK 实现这个能力,你第一步会怎么设计?为什么不直接做成一个 Tool,让模型传客户 ID 列表就删?
候选人:结论先说:我不会把“直接删除”做成一个一次调用就生效的 Tool,而是拆成“预检查/生成影响面”和“确认执行”两个阶段,并且保留只读的审计查询能力。
原因有三点。第一,MCP 的 Tool 是模型可发起调用的操作,适合有副作用的动作;Resources 是只读上下文,Prompts 是用户可显式选择的模板化工作流,三者不能混用。只读资料不应强行设计成有副作用的 Tool [资料1]。第二,批量删除属于高风险、不可逆操作,按照安全边界要求,必须展示具体影响,并在执行前要求用户确认,不能因为请求来自 AI 应用就默认可信 [资料1]。第三,参数 schema 只能做结构约束,不能代替服务端校验和授权,所以即使模型传进来的是看起来合法的客户 ID 数组,服务端仍然要把它当成不可信输入处理 [资料1]。
我会先定义三类能力:
customerData.previewDeletion:Tool,输入过滤条件或客户 ID 列表,返回将被删除的记录数量、关联附件、影响业务范围、风险提示,但不执行删除。customerData.confirmDeletion:Tool,输入上一步生成的确认令牌(confirmation token)和用户显式确认结果,真正执行删除。audit.recentDeletionLogs:Resource 或只读 Tool,按 URI 或参数查询近期操作日志,方便 Host 展示给用户做复核。
这样模型不能跳过预览直接删,用户也能看到明确影响面。
二、追问一:stdio 传输下,协议和调试日志怎么不互相污染?
面试官:你提到本地 AI 助手通过 stdio 启动 Server。stdio 看起来简单,但最容易踩什么坑?
候选人:stdio 适合 Host 在本机启动子进程的场景。这里最容易踩的坑是:把调试日志写到标准输出,会直接破坏 MCP 通信。因为 MCP 基于 JSON-RPC,Server 的标准输出用于协议消息;调试日志必须写到标准错误 [资料1]。
如果用 TypeScript MCP SDK 启动 stdio Server,我的设计原则是:
- 协议消息:只走 stdout,由 SDK 负责序列化 JSON-RPC。
- 调试、错误、审计落盘前的缓冲日志:走 stderr,避免污染协议流。
- 进程退出码:只用于表达 Server 自身启动失败、致命异常,不用来承载业务结果;业务成功/失败必须通过 JSON-RPC 响应返回。
一个版本无关的接口设计可以这样表达:
// 伪代码:仅说明职责边界,不对应具体 SDK 版本 API const server = createMcpServer({ name: "customer-risk-control-server", capabilities: { tools: {}, resources: {} } }); server.tool( "customerData.previewDeletion", "预览待删除客户资料,返回影响面与确认令牌,不执行删除", previewInputSchema, async (args, ctx) => { ctx.log.warn("previewDeletion called", { requestedBy: ctx.userId }); // 写 stderr const preview = await buildDeletionPreview(args, ctx); return { content: [{ type: "text", text: formatPreview(preview) }], structuredContent: { affectedCount: preview.affectedCount, confirmationToken: preview.confirmationToken, expiresAt: preview.expiresAt, warning: preview.warning } }; } );这里我刻意把“确认令牌”设计成短时有效、服务端签名、绑定具体影响面和操作者,而不是让模型自己传一个“确认=true”。否则模型可能误传,或者上一次预览和下一次确认之间参数被替换。
面试官:如果 stdio 子进程被恶意参数启动怎么办?比如用户机器上还有别的 MCP Client,能不能直接调用你的删除 Tool?
候选人:这就涉及最小权限。stdio 传输下,MCP 代理或 Host 应该对派生进程做额外控制,包括沙箱或容器化、限制文件系统访问、记录所有 stdio 使用日志,并对危险命令增加额外授权 [资料3]。我的 Server 侧还要做两层防御:
- 进程启动时权限收敛:不给 Server 进程过高的本地文件、网络和数据库权限;删除能力只授予最小范围的服务账号。
- 每次调用都鉴权:不能因为是本机 stdio 就信任调用方。远程 Server 需要每次请求做授权检查,这一原则在本地高风险场景同样适用 [资料1][资料2]。TypeScript SDK 本身不替代授权实现,鉴权逻辑要接入宿主环境的用户身份上下文,而不是只看“是否连上了”。
三、追问二:人工确认到底放在哪一层?异常时怎么保证不丢审计?
面试官:你说执行前要人工确认。这个确认应该由 MCP Server 弹窗,还是由 Host/客户端弹?如果确认到一半网络断了、进程崩了、用户点完确认又回滚,会发生什么?
候选人:结论是:确认交互应由 Host 面向用户展示,Server 只负责生成可验证的确认令牌并校验确认结果。原因是 MCP Server 在 stdio 场景下是子进程,不应该直接抢 UI;而且安全规范要求高风险操作显示具体影响并由用户确认,展示层属于 Host 的责任边界 [资料1]。
完整流程我会设计成六步:
- 模型调用
customerData.previewDeletion,提交待删条件。 - Server 做参数校验、权限校验、影响面计算,生成
confirmationToken。令牌中绑定用户 ID、参数哈希、影响摘要、过期时间,并由服务端签名。 - Server 返回可读的影响说明和结构化令牌,但不执行删除。
- Host 把影响说明展示给用户,要求显式确认;用户确认后,Host 带着同一个令牌调用
customerData.confirmDeletion。 - Server 再次校验令牌:签名是否有效、是否过期、当前用户是否一致、参数是否与预览一致、权限是否仍然有效。全部通过才执行删除。
- Server 写入审计日志,返回执行结果;如果失败,返回明确错误码和可重试建议。
异常处理上,我不会做“静默重试”。
- 预览阶段异常:直接返回错误,不生成令牌,不写成功审计,但要记录失败尝试。
- 确认后执行阶段异常:如果数据库已经开始删除但中途失败,必须依赖底层事务或补偿逻辑保证结果可解释;MCP 层返回“部分成功/失败明细”,不能简单说成功。
- 进程崩溃:因为审计日志不能只靠 stdout,我会在执行真正删除前先写一条“待执行”审计记录,执行结束后更新为成功或失败;日志要记录谁在什么时间调用了哪个 Tool、关键资源范围与结果状态,并对敏感字段脱敏 [资料1]。
- 令牌丢失或过期:要求重新走预览,不提供“凭旧参数直接确认”的捷径。
审计字段我会至少包含:调用时间、用户标识、Tool 名称、输入参数摘要、影响资源范围、确认令牌 ID、执行结果、错误分类、客户端会话标识。凭据本身绝不能出现在日志、Tool 返回值或模型上下文中 [资料1]。
四、方案设计题:最小权限、审计和可观测性如何协作?
面试官:如果让你把最小权限、审计日志、stdio 传输、TypeScript SDK 串成一个可落地方案,你会怎么划分职责?有什么取舍?
候选人:我会按“传输层、鉴权层、业务层、审计层”四层拆开。
1)传输层:stdio 只负责本机可靠通信
stdio 用于 Host 启动本地子进程,优点是部署简单、延迟低、不需要开放端口;缺点是不适合远程多租户场景,远程部署应考虑 Streamable HTTP,并额外处理认证、授权、会话管理、限流和超时 [资料1]。这个场景是本地办公助手,因此选 stdio 是合理的,但必须把调试日志打到 stderr,避免协议污染 [资料1]。
2)鉴权层:最小权限,且每次调用都检查
Server 进程以低权限运行,文件系统和网络访问受 Host 或操作系统限制 [资料3]。Tool 层不相信任何传入参数:客户 ID 必须属于当前用户有权访问的范围;过滤条件不能拼接成不受控 SQL;批量大小要设上限,上限值根据业务 SLA 和压测确定,不拍脑袋写死。MCP SDK 提供的是协议能力,不是完整授权系统;授权需要接入现有身份体系 [资料2]。
3)业务层:两阶段提交式人工确认
预览与确认分离,核心是把“模型建议”和“用户授权”分开。模型只能提出操作计划,不能越过用户确认直接执行不可逆动作。确认令牌必须绑定预览结果,避免 TOCTOU(检查时刻与使用时刻不一致)问题:预览时看到 3 条,确认时如果底层数据变了,Server 应拒绝执行并要求重新预览。
4)审计层:不可抵赖、可排错、可脱敏
审计不是打印 console.log。我会把审计写到独立日志通道,stderr 只放开发调试信息;高风险操作要能回答“谁在什么时候删了什么、依据哪次预览、用户是否确认、结果如何”。敏感字段如身份证号、联系方式要脱敏,凭据和令牌全文不能落日志 [资料1]。
关键取舍有两个:
- 交互复杂度 vs 安全性:两阶段确认会增加一次 Tool 调用,也会让模型调用链更长,但对于不可逆删除是必须的;只读查询或低风险操作不需要这么重。
- 本地便利性 vs 隔离成本:stdio 让本地接入简单,但 Host 仍应做进程级沙箱、文件系统限制和使用日志记录 [资料3]。如果做不到这些隔离措施,就不适合在 stdio 下暴露高风险 Tool。
容易踩坑的细节:很多人会把“用户确认”做成 Tool 参数里的一个布尔值,比如confirm: true。这是不安全的,因为模型可能在没有真实用户输入时自行补全这个字段。正确做法是:确认信息必须来自 Host 捕获的真实用户交互,并且与服务端签发、绑定上下文的令牌配对。
面试官点评
考察点:这道题不是考你会不会背 MCP 三个能力,而是看你能不能把协议能力、传输方式、安全边界和工程落地串起来。重点包括:
- 是否能正确区分 Tool、Resource、Prompt 的语义 [资料1];
- 是否理解 stdio 传输的适用场景与日志边界 [资料1];
- 是否知道 schema 校验不等于授权,服务端必须把输入视为不可信 [资料1];
- 是否能为高风险操作设计人工确认流程,而不是只加一个
confirm参数; - 是否考虑最小权限、审计日志、异常恢复和可观测性 [资料1][资料3];
- 是否理解 SDK 只提供协议和扩展点,授权等安全能力需要自行接入 [资料2]。
合格回答应包含:两阶段 Tool 设计、stdio 下 stdout/stderr 分离、服务端参数与权限校验、基于令牌的用户确认、审计日志字段设计,以及对异常路径的处理。
加分项:能明确说出适用边界——例如 stdio 适合本地子进程场景,不适合直接暴露远程高风险服务;能指出确认交互应放在 Host 层而不是 Server 弹 UI;能意识到 TOCTOU、凭据泄漏、日志污染这些工程细节;能说明哪些阈值(如批量大小、令牌有效期)需要根据业务 SLA、风险评估和压测确定,而不是凭空写数字。
总结
设计需要人工确认的高风险 MCP Tool,核心不是“把功能包成 Tool”这么简单,而是守住三条线:能力边界清晰、用户显式授权、每一步可审计可追责。stdio 传输降低了本地接入成本,但也要求更严格的进程隔离和日志规范;TypeScript MCP SDK 负责协议与能力暴露,不替你做授权和安全决策;最小权限和审计日志不是附加项,而是高风险操作能上线的前提。把这些点答清楚,才算真正理解了 MCP 在真实业务里的工程化用法。
参考资料
- MCP 基础知识
- Security Best Practices:https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices
- MCP TypeScript SDK:https://github.com/modelcontextprotocol/typescript-sdk
- MCP Java SDK:https://github.com/modelcontextprotocol/java-sdk