提交前确认 / Before submitting
类型 / Type:MCP(远程服务)
Operit 版本 / Operit version
1.12.1 (46)
设备与运行环境 / Environment
HONOR EBG-AN10 / Android 12
包或服务名称 / Package or service
hithinkfinance(远程 MCP / httpStream;第三方 A 股行情类服务)
来源与版本 / Source and version
来源:远程 MCP 服务(自行配置的第三方端点,非市场插件包)。
版本:服务端未声明版本号;initialize 协商的协议版本为 2024-11-05。
连接与相关配置 / Transport and configuration
- transport:httpStream(远程 MCP;HTTP POST + JSON)
- 涉及方法:initialize / notifications/initialized / tools/list / ping
- 端点鉴权已隐去(本 issue 不提供任何 Key)
Provider、模型与工具 / Provider, model and tool
不适用:问题发生在插件验证/注册阶段,未进入模型与工具调用环节。
复现步骤 / Steps to reproduce
【最小复现(本地、不依赖第三方服务;Python 3 标准库即可)】
- 在本机运行最小 MCP Streamable HTTP 服务端(脚本见下方折叠区):initialize / tools/list / ping 均按规范;唯独对 notifications/initialized 返回 200 + JSON 伪响应(自编 id + result),复刻线上服务端的非合规行为。
- 在 Operit 添加该端点:远程 MCP / httpStream,URL:http://127.0.0.1:8765/mcp
- 触发插件验证 → 预期与线上一致:不进入“验证成功”名单、不注册工具、无工具缓存。
- 对照组:把步骤 1 的伪响应改为合规行为(不响应 / 202 空 body)→ 重新验证 → 通过。
最小复现脚本(repro_min.py + selftest.py)
repro_min.py
#!/usr/bin/env python3
"""
Minimal repro: a Streamable-HTTP MCP server that answers a *notification* with a
pseudo-response, mimicking a third-party remote MCP service, which replies to
`notifications/initialized` with:
{"id":"<uuid>","result":{"_notification_handled":true}}
Per JSON-RPC 2.0 a notification MUST NOT get any response.
Per MCP Streamable HTTP (2025-03-26..2025-11-25 line) a POST containing only
notifications MUST be answered with HTTP 202, no body.
The initialize response below is kept *minimal but spec-compliant*
(Lifecycle, 2025-11-25): protocolVersion echoed; capabilities {} (all optional);
serverInfo requires only name+version.
Run: python3 repro_min.py # listens on http://127.0.0.1:8765/mcp
"""
import json, uuid
from http.server import BaseHTTPRequestHandler, HTTPServer
HOST, PORT = '127.0.0.1', 8765
class Handler(BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
def log_message(self, fmt, *args): # keep quiet
pass
def _send_json(self, obj, code=200, headers=None):
raw = json.dumps(obj).encode('utf-8')
self.send_response(code)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(raw)))
for k, v in (headers or {}).items():
self.send_header(k, v)
self.end_headers()
self.wfile.write(raw)
def _send_empty(self, code):
self.send_response(code)
self.send_header('Content-Length', '0')
self.end_headers()
def do_GET(self):
# Optional SSE stream not implemented; spec: servers either offer it or reply 405.
self.send_response(405)
self.send_header('Content-Length', '0')
self.end_headers()
def do_POST(self):
length = int(self.headers.get('Content-Length') or 0)
body = self.rfile.read(length) if length else b''
try:
msg = json.loads(body)
except Exception:
self._send_empty(400)
return
mid, method = msg.get('id'), msg.get('method')
if method == 'initialize':
req_v = (msg.get('params') or {}).get('protocolVersion', '2025-11-25')
self._send_json(
{'jsonrpc': '2.0', 'id': mid, 'result': {
'protocolVersion': req_v,
'capabilities': {},
'serverInfo': {'name': 'repro-bad-server', 'version': '0.1.0'}}},
headers={'Mcp-Session-Id': 'repro-session-0001'})
elif method == 'notifications/initialized':
# ===== the non-compliant bit (mimics the observed remote server) =====
# A notification must get NO response; here we send a pseudo-response.
self._send_json({'jsonrpc': '2.0', 'id': str(uuid.uuid4()),
'result': {'_notification_handled': True}})
elif method == 'tools/list':
self._send_json({'jsonrpc': '2.0', 'id': mid, 'result': {'tools': []}})
elif method == 'ping':
self._send_json({'jsonrpc': '2.0', 'id': mid, 'result': {}})
elif mid is None:
# any other notification: correct behavior - accept with 202, no body
self._send_empty(202)
else:
self._send_json({'jsonrpc': '2.0', 'id': mid,
'error': {'code': -32601, 'message': 'Method not found'}})
if __name__ == '__main__':
print(f'repro server on http://{HOST}:{PORT}/mcp')
HTTPServer((HOST, PORT), Handler).serve_forever()
selftest.py
#!/usr/bin/env python3
# Self-test for repro_min.py: verify the two behavioral branches over raw HTTP.
import json, urllib.request
def post(m):
req = urllib.request.Request(
'http://127.0.0.1:8765/mcp',
data=json.dumps(m).encode(),
headers={'Content-Type': 'application/json'},
method='POST')
r = urllib.request.urlopen(req, timeout=5)
return r.status, r.read().decode()[:200]
st, body = post({'jsonrpc': '2.0', 'id': 1, 'method': 'initialize',
'params': {'protocolVersion': '2025-11-25', 'capabilities': {},
'clientInfo': {'name': 'selftest', 'version': '0.0'}}})
print('INIT ->', st, body)
st, body = post({'jsonrpc': '2.0', 'method': 'notifications/initialized'})
print('INITD ->', st, body)
st, body = post({'jsonrpc': '2.0', 'id': 2, 'method': 'tools/list'})
print('TOOLS ->', st, body)
主要问题描述 / Main problem description
【实际】远程 MCP(httpStream)插件在“插件验证”阶段失败:不进入“验证成功”名单、不注册工具、无工具缓存,工具面板不可用;用户侧没有任何可定位的协议层提示。
【定位】该服务端对 notifications/initialized(通知;JSON-RPC 2.0 规定不得有任何响应,MCP Streamable HTTP 应为 202 空 body)返回 200 + JSON 伪响应:
{"jsonrpc":"2.0","id":"<服务端自行生成,不指向任何真实请求>","result":{"_notification_handled":true}}
该“无主响应”无法与任何在途请求匹配,严格校验的客户端会视为协议违规——疑似即验证失败的直接原因。
【已排除】网络与配置正常:同一端点 initialize / ping 均正常返回 200。
【期望】1) 对这类非合规响应:即便出于兼容而容忍,建议“忽略并计数”(可观测),不因此判插件失败;或至少在验证失败时给出协议层原因(如 unmatched response),便于用户定位。2) 同步确认:客户端对“无主响应”是否有兼容/容忍计划。
【影响】第三方服务不合规时,用户仅表现为“插件不可用”,排障成本高(需自行取证到协议层才能定位)。
截图或录屏 / Screenshot or recording
(暂未上传;以文字与日志补充说明。)
日志、请求或响应 / Logs, request or response
【日志】operit.log(2026-09-12,节选)
· 06:04:30.892 连接 MCP runtime: hithinkfinance …(连接尝试)
· 06:04:32.176 D/MCPStarter: 开始为 4 个验证成功的插件注册工具: [sinafinance, taiwanmarket, yahoofinance, koreamarketmcp](本插件不在其中)
(检索关键字:验证成功的插件)
【状态文件】server_status.json 中 hithinkfinance:lastStartTime:0 / lastStopTime:0 / toolsCachedTime:0,无 cachedTools(对照:sinafinance 已缓存 75 个工具)。
【服务端行为】对 notifications/initialized 的响应:
HTTP 200
{"jsonrpc":"2.0","id":"<服务端自生成,无对应请求>","result":{"_notification_handled":true}}
(规范要求:不得有响应;Streamable HTTP 应为 202 空 body)
【版本口径】复现固定 2025-11-25(含握手代);实测服务端协商 2024-11-05;2026-07-28 已取消握手,本复现不涉新版语义。
【相关】#1050(MCP 引擎与工具注册域跟踪)可能相关,供判断。
【脱敏确认】全文不含 API Key / Token / Cookie 等敏感信息。
提交前确认 / Before submitting
类型 / Type:MCP(远程服务)
Operit 版本 / Operit version
1.12.1 (46)
设备与运行环境 / Environment
HONOR EBG-AN10 / Android 12
包或服务名称 / Package or service
hithinkfinance(远程 MCP / httpStream;第三方 A 股行情类服务)
来源与版本 / Source and version
来源:远程 MCP 服务(自行配置的第三方端点,非市场插件包)。
版本:服务端未声明版本号;initialize 协商的协议版本为 2024-11-05。
连接与相关配置 / Transport and configuration
Provider、模型与工具 / Provider, model and tool
不适用:问题发生在插件验证/注册阶段,未进入模型与工具调用环节。
复现步骤 / Steps to reproduce
【最小复现(本地、不依赖第三方服务;Python 3 标准库即可)】
最小复现脚本(repro_min.py + selftest.py)
repro_min.py
selftest.py
主要问题描述 / Main problem description
【实际】远程 MCP(httpStream)插件在“插件验证”阶段失败:不进入“验证成功”名单、不注册工具、无工具缓存,工具面板不可用;用户侧没有任何可定位的协议层提示。
【定位】该服务端对 notifications/initialized(通知;JSON-RPC 2.0 规定不得有任何响应,MCP Streamable HTTP 应为 202 空 body)返回 200 + JSON 伪响应:
{"jsonrpc":"2.0","id":"<服务端自行生成,不指向任何真实请求>","result":{"_notification_handled":true}}
该“无主响应”无法与任何在途请求匹配,严格校验的客户端会视为协议违规——疑似即验证失败的直接原因。
【已排除】网络与配置正常:同一端点 initialize / ping 均正常返回 200。
【期望】1) 对这类非合规响应:即便出于兼容而容忍,建议“忽略并计数”(可观测),不因此判插件失败;或至少在验证失败时给出协议层原因(如 unmatched response),便于用户定位。2) 同步确认:客户端对“无主响应”是否有兼容/容忍计划。
【影响】第三方服务不合规时,用户仅表现为“插件不可用”,排障成本高(需自行取证到协议层才能定位)。
截图或录屏 / Screenshot or recording
(暂未上传;以文字与日志补充说明。)
日志、请求或响应 / Logs, request or response
【日志】operit.log(2026-09-12,节选)
· 06:04:30.892 连接 MCP runtime: hithinkfinance …(连接尝试)
· 06:04:32.176 D/MCPStarter: 开始为 4 个验证成功的插件注册工具: [sinafinance, taiwanmarket, yahoofinance, koreamarketmcp](本插件不在其中)
(检索关键字:验证成功的插件)
【状态文件】server_status.json 中 hithinkfinance:lastStartTime:0 / lastStopTime:0 / toolsCachedTime:0,无 cachedTools(对照:sinafinance 已缓存 75 个工具)。
【服务端行为】对 notifications/initialized 的响应:
HTTP 200
{"jsonrpc":"2.0","id":"<服务端自生成,无对应请求>","result":{"_notification_handled":true}}
(规范要求:不得有响应;Streamable HTTP 应为 202 空 body)
【版本口径】复现固定 2025-11-25(含握手代);实测服务端协商 2024-11-05;2026-07-28 已取消握手,本复现不涉新版语义。
【相关】#1050(MCP 引擎与工具注册域跟踪)可能相关,供判断。
【脱敏确认】全文不含 API Key / Token / Cookie 等敏感信息。