MCP: serve tools from the API process over Streamable HTTP at /mcp

- netx_api/mcp_remote: ops / topology / bizMonitor tools, Bearer auth,
  per-tool RBAC filtering, ?groups= filter, readOnlyHint and group meta.
- Mount the official MCP Streamable HTTP app at /mcp (stateless);
  AuthAuditMiddleware is pure ASGI and skips /mcp.
- Let uvicorn chunk MCP responses; some Windows LSPs rewrite
  Content-Length replies on */mcp* paths into broken chunked framing.
- NETX_MCP_ALLOWED_HOSTS opts into DNS-rebinding checks (off by default
  so LAN and reverse-proxy hosts work; Bearer is the gate).
- Remove the three stdio Python MCP packages; tests moved to /mcp,
  including real-TCP framing regressions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-16 21:52:08 +08:00
parent 918ece86c3
commit 52679a7cfc
150 changed files with 2367 additions and 2812 deletions

View file

@ -0,0 +1,5 @@
"""Ops MCP tools (in-process handlers for built-in /mcp)."""
from .http_tools import HTTP_MCP_TOOLS, call_http_tool
__all__ = ["HTTP_MCP_TOOLS", "call_http_tool"]

View file

@ -0,0 +1,26 @@
"""MCP 独立只读边界:后端开放 shell 策略也不能扩大模型的执行权限。"""
from __future__ import annotations
import re
_PREFIX = re.compile(r"(?i)^(show|display)[ \t]+\S")
_CONTROL = re.compile(r"[\x00-\x08\x0a-\x1f\x7f-\x9f\u2028\u2029]")
_SHELL = re.compile(r"[;&`$<>]")
_DESTRUCTIVE = re.compile(
r"(?i)\b(reboot|reload|shutdown|erase|format|delete|commit|rollback|redirect|append|tee)\b"
r"|\b(?:configure\s+terminal|conf\s+t|write\s+memory|system-view|undo\s+)"
)
_FILTER = re.compile(r"(?i)^(include|exclude|begin|section|count|match|grep|one-line|no-more)(?:[ \t]|$)")
def validate_readonly_cli(command: str) -> None:
if not isinstance(command, str) or not command.strip():
raise ValueError("MCP CLI requires a nonempty show/display command")
if len(command) > 500 or _CONTROL.search(command) or _SHELL.search(command):
raise ValueError("MCP CLI rejects control characters, shell operators and commands longer than 500 characters")
cmd = command.strip()
if not _PREFIX.match(cmd) or _DESTRUCTIVE.search(cmd):
raise ValueError("MCP CLI allows read-only show/display only; open device policies do not override this boundary")
parts = cmd.split("|")
if any(not _FILTER.match(part.strip()) for part in parts[1:]):
raise ValueError("MCP CLI pipes allow output filters only")

File diff suppressed because it is too large Load diff