本页目录

MCP与工具集成

认识 MCP 的协议、传输、工具与授权边界,区分新旧版本并运行离线消息实验。

L2 · 能交付约 20 分钟阅读含示例、练习与验收

建议先读:工具调用的完整闭环工作流与单Agent

本页内容

MCP 解决连接方式的一致性#

当一个助手要接文件系统、工单服务和内部知识库,如果每个服务都给不同格式的工具描述与结果,客户端就要维护许多适配器。Model Context Protocol 提供描述能力、发送调用、读取资源和交换结果的共同协议。它主要解决互操作,不自动让模型更聪明,也不把所有接入服务都变成可信来源。

本章用于读懂 MCP 集成的组成部分。达标后,你应能区分宿主应用、客户端、服务端与模型,知道请求在哪一层经过授权,并能解释为什么“连接成功”不等于“可以执行任何操作”。前置是 JSON-RPC 概念、工具调用及单 Agent 的执行边界。

宿主是面向用户的应用,负责会话和整体策略;客户端是其中处理 MCP 通信的组件;服务端暴露某个业务域的工具与资源。模型通常经宿主获得经过选择的工具描述,再提出调用建议。客户端负责协议通信,服务端执行自己的能力;具体架构也可能包含远程代理,但权限责任不能因此消失。

版本必须写清楚#

本章在二〇二六年九月十日核对官方规范,采用 2026-07-28 版本说明。该版以每次请求携带版本与客户端能力为基础,不使用旧版 initialize 握手。常见二〇二五年教程则基于 2025-11-25 或更早版本,先初始化协商再进入会话。两套流程不能随意混用。官方版本与兼容说明

规范更新不代表你安装的 SDK、宿主和服务端都已经支持。接入前记录三方具体版本及支持的协议版本,按实际交集运行;如果仍用旧版 SDK,就查对应旧版文档。不要把本文的新版本请求直接发给旧服务,再把失败简单归为“密钥错误”。

新版每请求元数据含必需的 protocolVersion 与 clientCapabilities,clientInfo 为建议提供的自报名称版本,不是可信身份。HTTP 绑定还规定对应头部。版本不受支持时有明确协议错误,客户端应选择共同支持版本或清楚报错,不能静默把某些字段删掉假装兼容。基础协议字段

工具、资源和提示不是同一件事#

tools 描述可执行能力,例如查询工单。resources 表达可读取的上下文数据,通常由 URI 标识。prompts 是服务端提供的提示模板能力。它们都可以帮助宿主组织模型上下文,但资源内容和模板文本仍需要按来源评估,不能自动获得应用最高指令优先级。

MCP 工具的 inputSchema 是 JSON Schema;工具列表用 tools/list,调用用 tools/call,并传 name 与 arguments。响应可以有文本内容及结构化内容。协议层错误与工具业务失败分开:请求方法或结构错误属于 JSON-RPC error,执行中的业务失败可通过工具结果 isError 表达。新版结果还要检查 resultType,不能把“需要更多输入”当成已经完成。工具规范

模型供应商的 function calling 与 MCP 也不同。前者描述模型怎样提出函数调用,后者描述客户端与能力服务怎样通信。宿主可能把 MCP 工具转换成某供应商的工具 schema,但这是一层适配,涉及 schema 子集、名字冲突、内容类型和错误映射,不能认为字段名称天然完全一致。

完整实验:离线构造与读取新版消息#

环境:Node.js 22;文件 mcp-messages.mjs;无依赖;运行 node mcp-messages.mjs。这是完整可运行的消息实验,不是可连接宿主的 MCP 服务端。它只覆盖 tools/list 与 tools/call 的一个只读 fixture,不实现传输、发现、OAuth、分页或全部规范。

js
import assert from 'node:assert/strict';

const version = '2026-07-28';
const prefix = 'io.modelcontextprotocol/';
let requestId = 0;
function request(method, params = {}) {
  return {
    jsonrpc: '2.0', id: ++requestId, method,
    params: {
      ...params,
      _meta: {
        [prefix + 'protocolVersion']: version,
        [prefix + 'clientCapabilities']: {},
        [prefix + 'clientInfo']: { name: 'lesson-client', version: '1.0.0' },
      },
    },
  };
}

const tool = {
  name: 'get_ticket',
  description: '读取当前用户可访问工单的公开状态。',
  inputSchema: {
    type: 'object', properties: { id: { type: 'string' } },
    required: ['id'], additionalProperties: false,
  },
};

function mockExchange(message) {
  const envelope = { jsonrpc: '2.0', id: message.id };
  const meta = message.params?._meta;
  if (!meta || !Object.hasOwn(meta, prefix + 'protocolVersion') ||
      !Object.hasOwn(meta, prefix + 'clientCapabilities')) {
    return { ...envelope, error: { code: -32602, message: '缺少必要元数据' } };
  }
  if (meta[prefix + 'protocolVersion'] !== version) {
    return { ...envelope, error: {
      code: -32022, message: 'Unsupported protocol version',
      data: { supported: [version], requested: meta[prefix + 'protocolVersion'] },
    } };
  }
  if (message.method === 'tools/list') {
    return { ...envelope, result: { resultType: 'complete', tools: [tool] } };
  }
  if (message.method !== 'tools/call') {
    return { ...envelope, error: { code: -32601, message: 'Method not found' } };
  }
  if (message.params.name !== tool.name) {
    return { ...envelope, error: { code: -32602, message: 'Unknown tool' } };
  }
  const args = message.params.arguments;
  const valid = args && typeof args === 'object' && !Array.isArray(args) &&
    Object.keys(args).length === 1 && typeof args.id === 'string';
  // 固定 T1 可见仅为 fixture,不是完整业务鉴权。
  const ok = valid && args.id === 'T1';
  return { ...envelope, result: {
    resultType: 'complete', isError: !ok,
    content: [{ type: 'text', text: ok ? 'T1:处理中' : '当前无法读取该工单' }],
  } };
}

function readResponse(sent, received) {
  assert.equal(received.jsonrpc, '2.0');
  assert.equal(received.id, sent.id); // 防止把并发请求的结果关联错误。
  if (received.error) throw new Error(received.error.message);
  if (received.result?.resultType !== 'complete') {
    throw new Error('此实验只支持 complete 结果');
  }
  return received.result;
}

const listRequest = request('tools/list');
console.log(readResponse(listRequest, mockExchange(listRequest)).tools[0].name);
const callRequest = request('tools/call', { name: 'get_ticket', arguments: { id: 'T1' } });
const result = readResponse(callRequest, mockExchange(callRequest));
if (result.isError) throw new Error('工具执行失败');
console.log(result.content[0].text);

预期输出 get_ticket 与“T1:处理中”。把协议版本改成其他值但保持服务端支持版本不变,会收到版本错误;把工具名改成未知名称,会收到协议错误;把工单改成 T2,通信本身正常,但结果 isError 为 true。三种状态分别对应兼容、调用合同和业务执行问题。

request 在每次消息中放元数据,避免误以为同一连接第一次发过就能省略。readResponse 核对请求关联与结果类型,再由上层处理工具失败。模拟器只检查教学所需字段,不能据此宣称通过完整协议一致性测试。真实集成优先使用已经核实支持目标版本的官方 SDK。

传输与授权各解决什么#

stdio 通常由客户端启动本地子进程,经标准输入输出传递按行分隔的消息;协议输出不能混入普通 console 日志,应按实现约定把日志写入 stderr。Streamable HTTP 用 HTTP 请求承载消息,响应可能是 JSON 或请求范围内的 SSE 流。传输解决消息如何到达,协议解决字段是什么意思,两者都不等同于业务权限。传输规范

HTTP 授权规范基于 OAuth 相关机制,使客户端代表资源所有者访问受限服务;是否部署授权由实现选择,采用后需遵循其流程。stdio 通常从受控环境获取凭据,不照搬 HTTP OAuth 流。成功拿到令牌仍需要检查受众、范围、有效期及业务对象权限,不能把任何 Bearer token 转发到任意下游。授权规范

MCP 不会自动授予用户访问公司数据库的权限,也不会因为工具标注只读就使其实现一定只读。工具描述、注解和 clientInfo 都可能是自报信息。宿主应限制可暴露工具、显示敏感动作预览;服务端仍对每次读取与写入做授权。用户同意连接某服务与用户同意发送某封邮件,是不同层次的决定。

远程工具返回的文字还可能包含不可信指令。把它交给模型时保留来源与边界;后续动作继续经过正常执行器。服务端凭据应该只发给预期受众,不能为了“让工具更方便”把主密钥作为参数提供给模型。工具返回量、响应时间和调用频率也要有限制。

从消息实验走到真实集成#

下一步实践应选择一个只读业务域,固定 SDK 与协议版本,先验证发现或初始化流程、工具列表、参数校验、成功结果与业务错误,再增加鉴权和传输故障。不要同时接十个高权限服务,否则无法分清失败属于协议、账号还是业务逻辑。

前端应能说明连接到哪个服务、当前暴露哪些工具、调用是否完成及是否需要授权或确认。错误信息不宜只显示“连接失败”:版本不兼容、授权过期、参数错误和服务暂不可用有不同恢复动作。遥测至少关联业务任务、MCP 请求 ID、工具名和上游请求号,但不记录令牌。

从旧握手迁移时,哪些假设必须改变#

旧教程里的 initialize 不只是一个可以改名的请求,它建立了该协议时代的初始化与能力协商流程。按 2025-11-25 生命周期规范,客户端先发初始化请求,检查服务端返回的版本与能力,再发送 initialized 通知进入后续交互。若保留这一流程,却把后续消息按新版无状态规则解析,可能得到一种双方都没有承诺支持的混合实现。

新版把版本和客户端能力带到每个请求里,因此同一传输连接不再代表同一个对话上下文。一个 stdio 子进程可以接到来自多个任务的请求,服务端不能把上次调用保存的“当前工单”当成下一次调用的隐含参数。需要跨请求保留的业务状态应有显式标识,例如 ticketId 或 basketId,并在每次使用时核对身份和对象权限。显式标识解决定位,授权检查解决谁能使用它。

另外一个重要差异是服务端向客户端请求信息的方式。新版通过多轮往返结果请求额外输入,客户端获得信息后重新发起原操作,而不沿用旧式服务端主动发 JSON-RPC 请求的模式。这样一来,“收到一个结果对象”不一定代表业务已经完成,宿主需要处理 input_required,界面也可能进入等待用户输入状态。新版消息模式

协议版本选择与扩展能力选择应分开。两边都支持同一日期版本,不意味着都支持某个可选扩展。扩展失败时应按扩展合同退回核心行为或明确拒绝,不能把未知字段静默删掉后继续执行高影响操作。接入清单应分别记录核心版本、传输类型、工具能力及扩展能力,避免一个“兼容 MCP”的标签掩盖真实差异。

兼容探测还与传输有关。官方版本说明给 stdio 和 HTTP 定义了不同的现代与旧版识别路径,不能对所有错误统一回退 initialize。已经识别为现代协议的版本错误,应在双方支持的版本中选择;认证失败和暂时网络故障也不应被随意解释成“旧服务器”。教材中的离线计划器只展示已知版本交集,不实现这些真实探测细节。

示例二:根据已验证能力生成两种接入计划#

环境为 Node.js 22,无依赖。保存为 mcp-version-plan.mjs,执行 node mcp-version-plan.mjs,预期输出 modern 对应 tools/list,legacy 对应 initialize,无共同版本时报错。版本数组来自本地 fixture,代表人工或真实发现流程已经核实的能力;本例不连接服务器,也不把配置列表当成 SDK 自动检测结果。

mcp-version-plan.mjs
import assert from 'node:assert/strict';

const modern = '2026-07-28';
const legacy = '2025-11-25';
function plan({ sdkVersions, serverVersions }) {
  // 应用明确的偏好顺序,不通过日期字符串推断未知版本兼容。
  const version = [modern, legacy].find(v =>
    sdkVersions.includes(v) && serverVersions.includes(v));
  if (!version) throw new Error('NO_COMMON_PROTOCOL');
  if (version === modern) {
    return { era: 'modern', version, firstRequest: {
      jsonrpc: '2.0', id: 1, method: 'tools/list',
      params: { _meta: {
        'io.modelcontextprotocol/protocolVersion': version,
        'io.modelcontextprotocol/clientCapabilities': {},
        'io.modelcontextprotocol/clientInfo': {
          name: 'offline-course', version: '1.0.0'
        }
      }}
    }};
  }
  return { era: 'legacy', version, firstRequest: {
    jsonrpc: '2.0', id: 1, method: 'initialize',
    params: { protocolVersion: version, capabilities: {},
      clientInfo: { name: 'offline-course', version: '1.0.0' } }
  }};
}
function afterLegacyInitialize(response, planResult) {
  if (planResult.era !== 'legacy' || response.jsonrpc !== '2.0' ||
      response.id !== 1 || response.error ||
      response.result?.protocolVersion !== planResult.version) {
    throw new Error('INVALID_INITIALIZATION');
  }
  return { jsonrpc: '2.0', method: 'notifications/initialized' };
}
const current = plan({ sdkVersions: [modern, legacy], serverVersions: [modern] });
const old = plan({ sdkVersions: [legacy], serverVersions: [legacy, modern] });
assert.equal(current.firstRequest.method, 'tools/list');
assert.equal(old.firstRequest.method, 'initialize');
assert.equal(afterLegacyInitialize({
  jsonrpc: '2.0', id: 1, result: {
    protocolVersion: legacy, capabilities: {},
    serverInfo: { name: 'fixture', version: '1.0.0' }
  }
}, old).method, 'notifications/initialized');
assert.throws(() => plan({ sdkVersions: [legacy], serverVersions: [modern] }),
  /NO_COMMON_PROTOCOL/);
console.log({ modern: current.firstRequest.method, legacy: old.firstRequest.method });

这里的 preferred 顺序明确写为两个已知版本,而不是选择数组中日期最大的字符串。协议日期表示修订,不保证任意较新版本向后兼容所有旧语义。未知版本必须经过支持确认才能加入列表。程序还验证旧初始化响应的版本与预期一致,然后生成无 id 的通知,因为通知不等待响应。

真实接入时,计划器之后还需要完整响应验证、超时、断线和授权处理。旧初始化返回的能力不能只读版本就全部忽略,新版也需要保证每次发送的能力确实由客户端实现。示例有意把“如何选择一套一致流程”与“完整协议客户端”分开,避免一段几十行代码被误认为可以代替官方 SDK。

模型工具适配器不能只是重命名字段#

MCP 工具名只在所属服务范围内唯一。两个服务都提供 search 时,宿主应使用自身分配的服务标识建立命名空间,并保存模型可见名称到真实服务与工具名的映射。不能只拼接服务自报 name,因为两个服务可能自报相同名称。这个映射由应用管理,模型只能选择已登记项,不能通过构造字符串连接任意服务器。

inputSchema 描述 MCP 工具参数,但模型供应商可能只支持 JSON Schema 的某个子集。适配器需要检查可转换范围;无法表达的约束应保留在执行前的完整验证器里,必要时不向模型暴露该工具。删除复杂约束后仍声称 schema 完全等价,会使模型可生成的参数范围扩大。即使模型严格遵守转换后的合同,服务端仍要依据原合同校验。

输出同样存在两个消费方:模型需要足够的信息继续任务,界面可能需要结构化字段、资源链接或媒体内容。MCP 的 structuredContent 是服务端产生的数据,与模型的结构化生成不是一回事;输出 schema 存在时,应按其约定验证。新版可以表达的结构值范围也不应被旧版“总是对象”的类型假设限制。若只处理文本,应明确拒绝或忽略其他类型的产品策略,不能直接对所有内容 JSON.stringify 后丢给模型。

工具返回的链接也不是授权通行证。它可以指向需要再读取的资源,宿主应决定是否读取、用哪个服务解析以及是否允许外部访问。不要将远端返回的任意 URI 直接交给通用文件读取器或 HTTP 客户端。协议统一了描述方式,并没有消除应用对目标地址、大小、超时与权限的责任。

多轮往返中的三个标识不要混淆#

一次普通 JSON-RPC 请求的 id 用于把响应关联回当前请求;多轮往返中的 inputRequests 键用于把补充输入与问题对应;requestState 是服务端给出的不透明恢复材料。它们各有作用。重发补充输入时使用新的 JSON-RPC id,仍按原来的输入请求键返回内容,并原样带回 requestState,不能把它解析成自己的业务状态。

requestState 看起来可能像编码字符串,但客户端不应依赖其内部格式。服务器可能更换签名方式、加入版本或使用随机引用;客户端只负责按协议保留。服务端则应确保这个恢复材料不能被换到别人的对象或越权使用。一次多轮输入收集也不能自动等价于敏感操作确认;用户填写一个地区,与批准发送消息,是不同的业务意图。MRTR 官方规范

如果客户端不支持服务端需要的能力,不能假装已经获得用户输入。应报告能力不足或进入明确的降级路径。最多往返次数、每轮超时和取消后的状态由宿主控制,避免一个服务持续请求输入让任务永不结束。以下练习只演示一次地区补充,失败路径直接结束,不做无限重试。

练习:完成一次补充输入并区分三类失败#

实现一个只读售后规则工具。第一次请求缺少地区时返回 input_required;第二次用新请求编号提交地区并取得结果。要求请求元信息正确、可信身份有读取权限、不同租户不能访问同一资料。再验证错误版本、未知方法和业务不可读分别出现在哪一层。

参考答案:完整离线多轮消息实验

Node.js 22,无依赖。保存为 mcp-roundtrip.mjs,执行 node mcp-roundtrip.mjs,预期输出 roundTrips: 2、“七日内申请”与 capabilityCases: 4。这是特定只读工具的消息模拟器,不实现 HTTP、OAuth、完整 schema 引擎或通用 MRTR 客户端。

mcp-roundtrip.mjs
import assert from 'node:assert/strict';

const version = '2026-07-28';
const meta = {
  'io.modelcontextprotocol/protocolVersion': version,
  'io.modelcontextprotocol/clientCapabilities': { elicitation: { form: {} } }
};
const error = (id, code, message, data) => ({
  jsonrpc: '2.0', id, error: { code, message, ...(data ? { data } : {}) }
});
function request(id, extra = {}) {
  return { jsonrpc: '2.0', id, method: 'tools/call', params: {
    _meta: structuredClone(meta), name: 'get_policy',
    arguments: { productId: 'S-42' }, ...extra
  }};
}
const isRecord = value => value !== null && typeof value === 'object' && !Array.isArray(value);
function supportsForm(capabilities) {
  const elicitation = capabilities.elicitation;
  if (!isRecord(elicitation)) return false;
  // 旧声明 elicitation:{} 兼容表示仅支持 form,不等于未声明能力。
  return Object.keys(elicitation).length === 0 ||
    (Object.hasOwn(elicitation, 'form') && isRecord(elicitation.form));
}
function server(req, identity) {
  const m = req.params?._meta;
  if (!m || typeof m['io.modelcontextprotocol/protocolVersion'] !== 'string' ||
      !isRecord(m['io.modelcontextprotocol/clientCapabilities'])) {
    return error(req.id, -32602, 'Missing request metadata');
  }
  if (m['io.modelcontextprotocol/protocolVersion'] !== version) {
    return error(req.id, -32022, 'Unsupported protocol version',
      { supported: [version], requested: m['io.modelcontextprotocol/protocolVersion'] });
  }
  if (req.method !== 'tools/call') return error(req.id, -32601, 'Unknown method');
  if (req.params.name !== 'get_policy') return error(req.id, -32602, 'Unknown tool');
  const finish = (text, isError = false) => ({
    jsonrpc: '2.0', id: req.id,
    result: { resultType: 'complete', isError, content: [{ type: 'text', text }] }
  });
  // identity 模拟传输鉴权后的可信主体,不从 clientInfo 读取。
  if (identity.tenant !== 't1' || !identity.scopes.includes('policy:read')) {
    return finish('无权读取该资料', true);
  }
  if (req.params.arguments?.productId !== 'S-42') {
    return finish('没有该产品的可读资料', true);
  }
  const input = req.params.inputResponses?.market;
  if (!input) {
    if (!supportsForm(m['io.modelcontextprotocol/clientCapabilities'])) {
      return error(req.id, -32021, 'Form elicitation capability is required',
        { requiredCapabilities: { elicitation: { form: {} } } });
    }
    return { jsonrpc: '2.0', id: req.id, result: {
      resultType: 'input_required', requestState: 'opaque-fixture-state',
      inputRequests: { market: { method: 'elicitation/create', params: {
        mode: 'form', message: '请选择售后规则地区',
        requestedSchema: { type: 'object',
          properties: { region: { type: 'string', enum: ['CN', 'EU'] } },
          required: ['region'] }
      }}}
    }};
  }
  if (req.params.requestState !== 'opaque-fixture-state' ||
      input.action !== 'accept' || input.content?.region !== 'CN') {
    return finish('当前示例仅提供中国地区资料,或用户未接受输入', true);
  }
  return finish('七日内申请');
}
function read(req, response) {
  if (response.jsonrpc !== '2.0' || response.id !== req.id ||
      (Object.hasOwn(response, 'result') === Object.hasOwn(response, 'error'))) {
    throw new Error('INVALID_ENVELOPE');
  }
  if (response.error) return { kind: 'protocol_error', error: response.error };
  const result = response.result;
  const type = result.resultType ?? 'complete'; // 兼容旧版缺少此字段的结果。
  if (type === 'input_required') return { kind: 'needs_input', result };
  if (type !== 'complete') throw new Error('UNKNOWN_RESULT_TYPE');
  return { kind: result.isError ? 'tool_error' : 'complete', result };
}
const identity = { tenant: 't1', scopes: ['policy:read'] };
const first = request(1);
const pending = read(first, server(first, identity));
assert.equal(pending.kind, 'needs_input');
// 模拟用户在宿主表单里选择地区;不是模型自行批准写操作。
const second = request(2, {
  requestState: pending.result.requestState,
  inputResponses: { market: { action: 'accept', content: { region: 'CN' } } }
});
const done = read(second, server(second, identity));
assert.equal(done.kind, 'complete');
assert.notEqual(first.id, second.id);
const wrongVersion = request(3);
wrongVersion.params._meta['io.modelcontextprotocol/protocolVersion'] = '1900-01-01';
assert.equal(read(wrongVersion, server(wrongVersion, identity)).error.code, -32022);
const unknown = { ...request(4), method: 'unknown' };
assert.equal(read(unknown, server(unknown, identity)).error.code, -32601);
assert.equal(read(second, server(second, { tenant: 't2', scopes: ['policy:read'] }))
  .kind, 'tool_error');
const capabilityCases = [
  ['missing', {}, false],
  ['url-only', { elicitation: { url: {} } }, false],
  ['legacy-empty', { elicitation: {} }, true],
  ['explicit-form', { elicitation: { form: {} } }, true]
];
for (const [index, [name, capabilities, accepted]] of capabilityCases.entries()) {
  const req = request(10 + index);
  req.params._meta['io.modelcontextprotocol/clientCapabilities'] = capabilities;
  const response = server(req, identity);
  const parsed = read(req, response);
  if (accepted) {
    assert.equal(parsed.kind, 'needs_input', name);
    assert.equal(parsed.result.inputRequests.market.params.mode, 'form', name);
  } else {
    assert.equal(parsed.kind, 'protocol_error', name);
    assert.equal(parsed.error.code, -32021, name);
    assert.deepEqual(parsed.error.data.requiredCapabilities,
      { elicitation: { form: {} } }, name);
    assert.equal(Object.hasOwn(response, 'result'), false, name);
  }
}
console.log({ roundTrips: 2, text: done.result.content[0].text, capabilityCases: 4 });

新增四组能力断言分别覆盖没有 elicitation、仅支持 url、空 elicitation 对象和显式 form。前两种必须返回 -32021,并通过 requiredCapabilities 说明需要表单能力,不得同时返回 input_required;后两种允许请求表单。注意 clientCapabilities:{} 与 clientCapabilities:{elicitation:{}} 不同:前者完全没有声明输入收集能力,后者按兼容规则表示支持 form。不能只检查外层对象存在就发出表单请求。官方 elicitation 能力规则

第一次响应既不是异常,也不是可展示为最终答案的成功内容,而是一个需要额外输入的协议状态。第二次保留原工具和参数,添加 inputResponses 及原样恢复材料,同时更换请求编号。这个流程让宿主清楚知道何时等待用户、何时调用服务,也让每轮日志能够独立关联。

示例将 identity 作为独立参数传给模拟服务器,专门强调身份来自可信鉴权层。把 request._meta 里的 clientInfo 改成“管理员”不会改变它。真实 OAuth 接入还要验证签发者、受众、有效期、范围与授权流程,本例既未签发也未校验令牌,不应被称为 OAuth 实现。实际对象权限仍需由业务服务检查。

授权与工具目录缓存为什么会相互影响#

工具列表可以因请求所带授权不同而不同。若宿主把一个管理员读取的工具目录缓存给普通用户,模型可能看到本不该暴露的操作描述;即使最终执行被拒绝,也造成不必要的信息暴露和失败调用。缓存键应绑定服务配置与适当的授权范围,令牌或权限变化后应重新获取可用能力。列表变化通知可以帮助刷新,但不能代替执行时鉴权。

权限还可能在模型规划和真实调用之间变化。宿主在规划阶段筛选工具,服务端在执行阶段重新检查,是两个互补位置。客户端的“已连接”状态只说明配置或认证过程完成,不能证明某张工单当前可读。前端错误恢复也应区分连接配置、协议版本、授权过期、工具参数和业务对象不可用,让用户知道需要重新登录还是调整任务。

远程服务的工具描述和 schema 本身也是输入。限制说明长度、验证 schema、避免自动解引用任意网络地址,可以防止一个工具目录拖慢宿主或触发意外网络访问。具体支持的 schema 方言和引用处理应遵循所用协议版本及验证器能力;无法解析的约束应报告不支持,不要降级为允许任何参数。

对一次真实集成给出诚实的验收结论#

完成协议实验后,下一步应在受控只读服务上记录宿主、SDK、服务端和协议版本,验证工具目录、成功调用、工具错误、协议错误、授权失效及传输中断。至少测试一次返回不支持的结果类型和一次需要额外输入的结果,观察宿主是否错误地显示为“执行完成”。若 SDK 尚未支持新版 MRTR,就按其实际支持版本接入,不应手写几处字段来绕过整个生命周期差异。

日志中保留业务任务编号、模型 call_id、MCP 请求 id 和远端业务操作编号的映射,能把一次端到端故障拆到正确层面。它们不是同一个 ID,也不应互相覆盖。用户最终关心的是业务结果,工程师需要的是每一层的证据;一个好的适配器同时满足两者,并避免把底层令牌或敏感参数写入普通日志。

本章的三个程序分别验证消息结构、版本计划和多轮输入处理,仍未验证真实网络、SDK 互操作或身份提供方配置。上线前必须补这些联调证据。理解协议边界的价值,是知道下一次失败该查哪份合同、哪个版本和哪一层权限,而不是把所有错误都归结为“模型不会用工具”。

每次升级协议或 SDK 后,都应重新运行固定消息 fixture 并做真实只读互操作测试。仅通过 TypeScript 编译不能证明线上服务接受了同样的消息,离线构造正确也不能证明代理正确转发了头部、身份与流式响应。记录版本和验证范围,是后续维护者能够可靠复现接入结果的必要条件。

验收与三个自测#

验收要能识别三类错误、核对请求 ID、区分工具失败与协议失败、标明三方版本,并能指出真正的权限实施位置。本例未启动 MCP 服务、未运行 SDK 互操作、未执行 OAuth 或远程工具,不能把离线通过当成连接验证。

  1. 问:MCP 是一个自主 Agent 吗?答:不是,它提供能力交换协议,自主控制流由宿主与模型运行逻辑决定。
  2. 问:为何新版规范不能直接套到所有现有 SDK?答:各实现升级节奏与支持范围不同,必须确认共同版本。
  3. 问:连接并授权成功后可否跳过业务鉴权?答:不能,令牌权限与具体对象操作权限仍需按请求核验。

本章示例验证记录#

已离线运行新版消息、两版接入计划与多轮补充输入三个程序,验证请求关联、版本交集、未知方法、工具业务失败、新请求编号,以及缺少、仅 url、空声明和显式 form 四种客户端能力。未启动 MCP 服务、运行 SDK 互操作或执行 OAuth;规范版本不等于本机 SDK 支持声明。

官方资料#

本章使用 2026-07-28 版本兼容说明基础字段工具传输授权。阅读其他教程时先检查其规范版本。

原有课程整理于 2026-09-10;Node / Electron 扩充于 2026-09-11。示例环境与验证范围以正文为准。
原创中文学习手册,阅读结构参考 Vue 文档;非 Vue 官方教材。
下载本章 Markdown

支持中文和英文全文搜索 · ↑ ↓ 选择 · Enter 打开 · Esc 关闭