Build in Public · LF-10
远程 MCP 接入验收:能发现工具,离真正可用还差什么
用无 Key 的发现探针检查初始化、通知和工具目录,再分别核对 Claude、ChatGPT、Cursor 的网络与认证要求,避免把协议可见写成三端已实测。
把一个 MCP 地址填进客户端,看到工具名称出现,是有用的进展。但它还没有回答:密钥是否能以正确方式传递、实际调用是否属于允许的范围、错误是否被客户端识别,以及返回的数据是否满足任务要求。
本文以 EveryInfra 的远程入口为例,把接入验收分成协议发现、认证匹配、客户端操作和业务结果四层。先给出无需 Key 的发现探针,再解释三个客户端为何不能共用一句“填地址即可”。这是一份带实测边界的接入教程,不是 Claude、ChatGPT、Cursor 三端均已安装成功的声明。
一个远程地址,四层独立证据
本次核对的入口是 https://api.everyinfra.com/mcp。协议发现能告诉我们服务器如何自我描述、公开哪些工具;认证匹配解决客户端怎样获得并传递获准凭据;客户端验收检查工具是否真正进入当前会话;业务验收才核对实际结果和错误。
四层应分别记录。服务器响应了初始化,不代表客户端接受了后续通知;客户端看见了目录,不代表它有业务权限;工具返回内容,不代表其中没有业务错误。不要用“连接成功”的单一布尔值覆盖所有情况。
同样,MCP 与 REST 是不同接入形态。REST 样本可以帮助核对业务含义,却不能单独证明某个客户端正确处理 MCP 的认证、内容块、错误或确认流程。
初始化之后,还有一个通知
本文按 MCP 2025-06-18 版本核对这一条发现路径,不宣称它是所有客户端的最新或唯一版本。客户端先发送 initialize,核对服务器选择的版本,再发送 notifications/initialized,之后才进入工具发现。后续 HTTP 请求应带上协商出的协议版本。协议生命周期
Streamable HTTP 的通知被接受时,应返回 HTTP 202 且没有响应体。服务端若在初始化时返回 Mcp-Session-Id,客户端需在后续请求中保留它;没有该响应头不能擅自编造一个会话。请求响应可能是 JSON 或 SSE,实际客户端要支持协议要求的传输形式。传输规范
下面的脚本只检查本次服务器使用的 JSON 响应分支,不是通用 MCP 客户端。遇到 SSE、分页目录或不支持的版本会停止,交给完整客户端继续验收;它不假装只解析一个 JSON 对象就覆盖了整个协议。
一段只做发现的诊断脚本
使用支持内置 fetch 的 Node.js 22 或以上版本。脚本不读取环境中的 Key,不发送 tools/call,也不安装或修改任何客户端。输出只保留状态、工具名称和兼容性问题,不打印服务器说明里的业务宣传。
node --input-type=module <<'JS'
const endpoint = 'https://api.everyinfra.com/mcp';
const supportedVersion = '2025-06-18';
const headers = {
'Content-Type': 'application/json',
Accept: 'application/json, text/event-stream',
};
const issues = [];
async function post(message) {
const response = await fetch(endpoint, {
method: 'POST', headers, redirect: 'error',
body: JSON.stringify(message),
signal: AbortSignal.timeout(20000),
});
return { response, text: await response.text() };
}
function rpcResult(reply, id) {
if (reply.response.status !== 200) {
throw new Error('RPC HTTP ' + reply.response.status);
}
if (!reply.response.headers.get('content-type')?.includes('application/json')) {
throw new Error('This probe only handles JSON; use a full MCP client');
}
const body = JSON.parse(reply.text);
if (body.jsonrpc !== '2.0' || body.id !== id ||
body.error || !body.result || typeof body.result !== 'object') {
throw new Error('Invalid or failed RPC response');
}
return body.result;
}
const initReply = await post({
jsonrpc: '2.0', id: 1, method: 'initialize',
params: {
protocolVersion: supportedVersion, capabilities: {},
clientInfo: { name: 'bip-discovery-only', version: '1.0' },
},
});
const init = rpcResult(initReply, 1);
if (init.protocolVersion !== supportedVersion ||
init.serverInfo?.name !== 'EveryInfra' || !init.capabilities?.tools) {
throw new Error('Unexpected version, server or tool capability');
}
headers['MCP-Protocol-Version'] = init.protocolVersion;
const session = initReply.response.headers.get('mcp-session-id');
if (session) headers['Mcp-Session-Id'] = session;
const notification = await post({
jsonrpc: '2.0', method: 'notifications/initialized',
});
if (notification.response.status !== 202) {
throw new Error('Initialization notification was not accepted');
}
const notificationBytes = Buffer.byteLength(notification.text);
if (notificationBytes !== 0) issues.push('notification_body_not_empty');
// Continue discovery for diagnosis only; never enter a business call.
const listed = rpcResult(await post({
jsonrpc: '2.0', id: 2, method: 'tools/list', params: {},
}), 2);
if (!Array.isArray(listed.tools) || listed.tools.length === 0 ||
listed.tools.some(tool => !tool.name || tool.inputSchema?.type !== 'object') ||
listed.nextCursor) {
throw new Error('Incomplete or unexpected tool list');
}
console.log(JSON.stringify({
observedAt: new Date().toISOString(),
protocolVersion: init.protocolVersion,
sessionHeaderPresent: Boolean(session),
notificationStatus: notification.response.status,
notificationBodyBytes: notificationBytes,
toolNames: listed.tools.map(tool => tool.name),
issues,
businessCallPerformed: false,
hostAcceptanceVerified: false,
}, null, 2));
if (issues.length) process.exitCode = 2;
JS退出码 0 只表示这段有限的发现检查没有发现差异,绝不是可发布或三端验收通过。退出码 2 表示已取得目录,但存在协议差异;其他错误会终止执行。连接故障与发现成功后的兼容性问题应分别处理。
本次发现成功了什么,又发现了什么问题
2026 年 9 月 4 日 20:43(北京时间)的无鉴权复查中,初始化返回 HTTP 200、协议版本 2025-06-18,未提供会话 ID 响应头。工具目录返回六个名称:
everyinfra_list_capabilitieseveryinfra_call_apieveryinfra_chateveryinfra_list_captcha_typeseveryinfra_solve_captchaeveryinfra_search
但初始化完成通知返回 HTTP 202 时,响应体是 JSON null,不是空体。这不符合上述版本对通知响应的要求。目录仍能读取,不足以证明所有客户端都会容忍这个差异;脚本应明确报出它,而不是悄悄忽略。
本轮没有调用业务工具,也没有确认客户端是否因此失败。后续需由服务维护者处理响应差异,再在实际客户端重测。当前可支持的说法是“公开发现路径已观察到六个工具,存在通知响应差异”,不能写成“全面兼容 MCP”。
Cursor:配置头支持与真实权限分开验证
Cursor 官方文档为远程服务器提供 url 和 headers 配置,并支持从环境变量插值。下面是待用户授权后使用的配置形状;本轮没有把它写入用户配置,也没有验证该客户端的实际调用。Cursor MCP 文档
{
"mcpServers": {
"everyinfra": {
"url": "https://api.everyinfra.com/mcp",
"headers": {
"Authorization": "Bearer ${env:EVERYINFRA_API_KEY}"
}
}
}
}项目当前服务端代码包含 Bearer 头解析,但源码存在这条路径并不证明今天的某个 Key 可用。准备实际接入时,确认客户端进程能读取指定环境变量,并核对 Key 的业务权限;不要把真实值写进受版本控制的配置、截图或错误报告。
随后在 Cursor 当前版本中刷新目录、选择单个工具,并保留用户确认。不能因为目录可见就打开所有工具自动执行;有业务权限的 Key 也不应被当成所有操作的长期授权。
Claude:远程连接器不走本机网络
Claude 的官方远程连接器说明指出,请求从 Anthropic 云端发出,包括 Claude Desktop 的远程连接器。这与桌面端本地配置中的本地 MCP 是不同机制。因此,本机能访问地址,只能证明本机路径;不能证明云端连接器也能访问。Claude 远程连接器说明
接入时应根据当前账户和组织权限检查连接器入口,再核对服务与客户端双方支持的认证流程。官方说明中的 OAuth 配置不能被改写成“任何 Bearer API key 都能填进去”。若认证模型不匹配,需要服务或受控适配方案先完成设计和验收,不能把密钥塞进 URL 当作通用替代。
这里不提供未经本账户验证的点击截图或“添加即成功”步骤。添加连接器、授权和最终确认会改变账户状态,应在用户批准后执行;协议探针无法代替这些证据。
ChatGPT 与 OpenAI API:两个不同的接入入口
ChatGPT Developer mode 官方文档列出 OAuth、无认证和混合认证;其中混合认证涉及无认证发现及按工具安全声明选择认证方式。发现无需 Key,不等于所有业务工具也可以无认证调用。ChatGPT Developer mode
本次工具目录未观察到工具级 securitySchemes 或 _meta 字段。这是该响应的观察,不是对整个服务认证实现的完整审计;但它足以说明,不能仅凭发现成功就宣称已满足 ChatGPT 混合认证接入要求。
OpenAI Responses API 的远程 MCP 配置又是另一条路径,包含 server_url、工具选择、认证及调用批准等参数。它不是 ChatGPT 界面里的配置说明;通过 API 能构造请求,也不能反向证明网页端已经接通。OpenAI MCP 与连接器指南
本轮只核对文档,未运行 OpenAI API 业务请求,也未替用户启用 Developer mode。若后续使用这条路径,应独立审查将发送给远程服务的数据范围,并保持敏感调用的批准步骤。
认证设计还应单独阅读 MCP 官方安全最佳实践教程。该页位于 2025-11-25 文档路径,说明令牌应面向正确的服务,反对未经验证地透传令牌;它不把第三方 Key 变成通用 MCP 凭据,也不替代上文按 2025-06-18 版本做的发现检查。
HTTP 200 之后,还要检查两层结果
MCP 的协议错误可以出现在 JSON-RPC 顶层 error;工具执行错误也可以通过 result.isError 表达。不能只检查 HTTP 200 或有一个文本块,就宣布业务成功。工具与错误规范
实际业务还应检查工具返回的数据结构:输出是不是预期对象,是否为空或仅部分完成,是否存在异步任务仍需查询。多内容块要按工具契约处理,不能默认首个文本块包含全部元数据。账单、用量与余额需要查看实际业务响应;发现目录不会提供一次业务调用的结算证明。
对于发送、购买、提交或其他有副作用的操作,客户端显示了工具,不代表用户已经授权执行。先确认目标与参数,再调用;重试前确定第一次结果是否已知。本文的无 Key 探针刻意不进入这一层。
一份可以交给下一位接入人的验收记录
每个客户端分别记录产品与版本、运行位置、传输和认证方式、安装/授权证据、实际工具名、脱敏输入、结果状态及错误。记录观察时间与允许的调用范围,不保存 Key 原文。成功样本、明确失败和未知结果应分别留证。
记录方法可参考 OWASP 日志指南的事件关联与数据排除原则:关联同一次操作,同时避免把令牌、会话或完整客户内容写入排障附件。日志只是验收证据的一部分,不替代客户端的实际行为检查。
本轮已完成的是公开发现与文档差异核查。还没有完成通知空体修复、三个客户端的安装与认证、获准业务调用及失败显示验收。只有这些项目各自取得证据,才能把教程的措辞从“待验证配置”改成“在指定版本实测通过”。
可以先运行本文的发现脚本,确认当前接口与记录是否仍一致,再按团队选定的客户端推进。不要为了凑齐一张兼容列表,同时改动三个账户或复制一份未经验证的配置。EveryInfra 文档入口