fix(mcp): 修复配置导入、凭据管理与协议边界

修复生命周期锁阻塞事件循环、旧连接回调误停新连接及超时契约不一致。

补齐 MCP JSON 兼容导入、密钥拆分与失败重试,修复 Header 大小写草稿丢失,迁移大小写敏感的环境变量凭据。

在 SSE 行拼接前限制缓冲大小,增加并发、迁移和流式输入回归测试;忽略本机 MCP 数据及 server.json/servers.json。

验证:后端 185 项、前端 54 项测试通过,前端生产构建、相关文件 Ruff 与暂存差异检查通过。
This commit is contained in:
2026-09-03 22:38:35 +08:00
parent 2f7066aa92
commit 9b8b10cdb1
13 changed files with 1097 additions and 80 deletions
+51 -2
View File
@@ -10,6 +10,7 @@ import asyncio
import json
import os
import queue
import re
import signal
import subprocess
import threading
@@ -49,6 +50,7 @@ MAX_MCP_MESSAGE_BYTES = 2 * 1024 * 1024
MAX_MCP_TOOL_RESULT_BYTES = 256 * 1024
MAX_MCP_TOOLS = 500
MAX_MCP_LIST_PAGES = 100
_SSE_NEWLINE = re.compile(rb"\r\n?|\n")
class McpBridgeError(RuntimeError):
@@ -1019,7 +1021,8 @@ class McpBridge:
if status.status == PluginHostState.unhealthy:
raise McpBridgeError(
"PLUGIN_HOST_UNAVAILABLE",
status.error or "MCP event stream became unavailable during startup.",
status.error
or "MCP event stream became unavailable during startup.",
status_code=503,
)
status.status = PluginHostState.ready
@@ -1383,12 +1386,58 @@ def _bounded_json_response(response: httpx.Response) -> dict[str, Any]:
return payload
def _bounded_sse_lines(response: httpx.Response):
"""Split UTF-8 lines without httpx.iter_lines()'s unbounded line buffer.
Check each segment before appending it, including partial/no-newline input.
SSE allows LF, CR and CRLF; a CRLF pair can span network chunks.
"""
pending = bytearray()
event_size = 0
skip_lf = False
first_line = True
for chunk in response.iter_bytes():
offset = 0
if skip_lf and chunk:
offset = int(chunk.startswith(b"\n"))
skip_lf = False
for match in _SSE_NEWLINE.finditer(chunk, offset):
start, end = match.span()
segment = memoryview(chunk)[offset:start]
if event_size + len(pending) + len(segment) + 1 > MAX_MCP_MESSAGE_BYTES:
raise McpBridgeError(
"MCP_HTTP_RESPONSE_INVALID", "MCP SSE event is too large."
)
pending.extend(segment)
line = pending.decode("utf-8", errors="replace")
event_size += len(pending) + 1
pending.clear()
if first_line:
line = line.removeprefix("\ufeff")
first_line = False
if not line:
event_size = 0
yield line
skip_lf = chunk[end - 1 : end] == b"\r" and end == len(chunk)
offset = end
tail = memoryview(chunk)[offset:]
if event_size + len(pending) + len(tail) > MAX_MCP_MESSAGE_BYTES:
raise McpBridgeError(
"MCP_HTTP_RESPONSE_INVALID", "MCP SSE event is too large."
)
pending.extend(tail)
if pending:
line = pending.decode("utf-8", errors="replace")
yield line.removeprefix("\ufeff") if first_line else line
def _iter_sse(response: httpx.Response):
event = "message"
event_id: str | None = None
data_lines: list[str] = []
size = 0
for line in response.iter_lines():
for line in _bounded_sse_lines(response):
size += len(line.encode("utf-8")) + 1
if size > MAX_MCP_MESSAGE_BYTES:
raise McpBridgeError(