mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 01:40:51 +08:00
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:
parent
918ece86c3
commit
52679a7cfc
150 changed files with 2367 additions and 2812 deletions
233
netx_api/mcp_remote/topology/layout_ops/compact_bbox.py
Normal file
233
netx_api/mcp_remote/topology/layout_ops/compact_bbox.py
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
"""Uniform bbox shrink toward eye portals — eye-safe compactness.
|
||||
|
||||
Hard gates (must all pass to accept a scale):
|
||||
- global crossings must not rise
|
||||
- footprint overlaps must stay zero
|
||||
- frozen portal_ids stay put
|
||||
|
||||
Optional soft: prefer scales that do not worsen edge_clearance hits.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from netx_api.mcp_remote.topology.layout_metrics import (
|
||||
count_edge_crossings,
|
||||
compute_edge_clearance,
|
||||
)
|
||||
from netx_api.mcp_remote.topology.layout_ops.graph_util import bbox
|
||||
from netx_api.mcp_remote.topology.layout_ops.orbit_sweep import _has_any_footprint_overlap
|
||||
from netx_api.mcp_remote.topology.layout_ops.state import LayoutParams, LayoutState, OpResult
|
||||
|
||||
|
||||
def _anchor(
|
||||
pos: dict[str, tuple[float, float]],
|
||||
frozen: set[str],
|
||||
) -> tuple[float, float]:
|
||||
pts = [pos[n] for n in frozen if n in pos]
|
||||
if len(pts) >= 1:
|
||||
return (
|
||||
sum(p[0] for p in pts) / len(pts),
|
||||
sum(p[1] for p in pts) / len(pts),
|
||||
)
|
||||
if not pos:
|
||||
return (0.0, 0.0)
|
||||
xs = [p[0] for p in pos.values()]
|
||||
ys = [p[1] for p in pos.values()]
|
||||
return ((min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0)
|
||||
|
||||
|
||||
def _area(pos: dict[str, tuple[float, float]]) -> float:
|
||||
if len(pos) < 2:
|
||||
return 1.0
|
||||
x0, y0, x1, y1 = bbox(pos)
|
||||
return max((x1 - x0) * (y1 - y0), 1.0)
|
||||
|
||||
|
||||
def _clr_hits(pos: dict[str, tuple[float, float]], links, names) -> int:
|
||||
ec = compute_edge_clearance(pos, links, names=names, top_n=1)
|
||||
if ec.get("edge_clearance_skipped"):
|
||||
return 10**9
|
||||
return int(ec.get("edge_clearance_hits") or 0)
|
||||
|
||||
|
||||
def compact_bbox(
|
||||
state: LayoutState,
|
||||
params: LayoutParams | None = None,
|
||||
*,
|
||||
frozen_ids: set[str] | None = None,
|
||||
portal_ids: list[str] | None = None,
|
||||
min_scale: float = 0.72,
|
||||
step: float = 0.03,
|
||||
max_clearance_slack: int = 80,
|
||||
outlier_only: bool = True,
|
||||
) -> OpResult:
|
||||
"""Probe shrink toward portals; keep best gated layout.
|
||||
|
||||
``outlier_only``: only move nodes farther than ~median radius from the
|
||||
portal anchor — avoids crushing the already-dense eye core into overlaps.
|
||||
"""
|
||||
import math
|
||||
|
||||
del params
|
||||
st = state.copy()
|
||||
pos0 = dict(st.positions)
|
||||
names = st.names
|
||||
links = list(st.links)
|
||||
frozen: set[str] = set(frozen_ids or ())
|
||||
for p in portal_ids or []:
|
||||
if p:
|
||||
frozen.add(str(p))
|
||||
if not pos0:
|
||||
return OpResult(
|
||||
state=st,
|
||||
moved=set(),
|
||||
op="compact_bbox",
|
||||
note="compact_bbox:empty",
|
||||
params={"error": "empty"},
|
||||
)
|
||||
|
||||
g0 = count_edge_crossings(pos0, links)
|
||||
if _has_any_footprint_overlap(pos0, names):
|
||||
return OpResult(
|
||||
state=st,
|
||||
moved=set(),
|
||||
op="compact_bbox",
|
||||
note="compact_bbox:overlaps_before",
|
||||
params={
|
||||
"error": "overlaps_before",
|
||||
"hint": "Refuse shrink while footprints already overlap.",
|
||||
},
|
||||
)
|
||||
clr0 = _clr_hits(pos0, links, names)
|
||||
area0 = _area(pos0)
|
||||
cx, cy = _anchor(pos0, frozen)
|
||||
|
||||
dists = {
|
||||
n: math.hypot(x - cx, y - cy)
|
||||
for n, (x, y) in pos0.items()
|
||||
if n not in frozen
|
||||
}
|
||||
mobile: set[str]
|
||||
if outlier_only and dists:
|
||||
# Farthest-K only: percentile bands often crush mid-ring nodes into overlaps.
|
||||
k = max(16, min(40, len(dists) // 6))
|
||||
mobile = {
|
||||
n
|
||||
for n, _ in sorted(dists.items(), key=lambda kv: kv[1], reverse=True)[:k]
|
||||
}
|
||||
else:
|
||||
mobile = set(dists.keys())
|
||||
|
||||
best_pos = pos0
|
||||
best_meta: dict[str, Any] = {
|
||||
"scale": 1.0,
|
||||
"crossings": g0,
|
||||
"clearance_hits": clr0,
|
||||
"area": round(area0, 1),
|
||||
"accepted": False,
|
||||
}
|
||||
best_key = (g0, area0, clr0)
|
||||
|
||||
scales: list[float] = []
|
||||
s = 1.0 - float(step)
|
||||
lo = max(0.5, float(min_scale))
|
||||
while s >= lo - 1e-9:
|
||||
scales.append(round(s, 4))
|
||||
s -= float(step)
|
||||
|
||||
for scale in scales:
|
||||
trial: dict[str, tuple[float, float]] = {}
|
||||
for nid, (x, y) in pos0.items():
|
||||
if nid in frozen or nid not in mobile:
|
||||
trial[nid] = (x, y)
|
||||
else:
|
||||
trial[nid] = (
|
||||
cx + (x - cx) * scale,
|
||||
cy + (y - cy) * scale,
|
||||
)
|
||||
if _has_any_footprint_overlap(trial, names):
|
||||
continue
|
||||
g1 = count_edge_crossings(trial, links)
|
||||
if g1 > g0:
|
||||
continue
|
||||
clr1 = _clr_hits(trial, links, names)
|
||||
if clr1 > clr0 + max(0, int(max_clearance_slack)):
|
||||
continue
|
||||
area1 = _area(trial)
|
||||
key = (g1, area1, clr1)
|
||||
if key < best_key:
|
||||
best_key = key
|
||||
best_pos = trial
|
||||
best_meta = {
|
||||
"scale": scale,
|
||||
"crossings": g1,
|
||||
"clearance_hits": clr1,
|
||||
"area": round(area1, 1),
|
||||
"accepted": True,
|
||||
}
|
||||
|
||||
moved = {
|
||||
n
|
||||
for n, xy in best_pos.items()
|
||||
if n in pos0
|
||||
and (
|
||||
abs(xy[0] - pos0[n][0]) > 0.05
|
||||
or abs(xy[1] - pos0[n][1]) > 0.05
|
||||
)
|
||||
}
|
||||
st.positions = best_pos
|
||||
st.last_moved = moved
|
||||
meta = {
|
||||
"mode": "compact_bbox",
|
||||
"anchor": [round(cx, 1), round(cy, 1)],
|
||||
"frozen_n": len(frozen),
|
||||
"mobile_n": len(mobile),
|
||||
"outlier_only": bool(outlier_only),
|
||||
"start_crossings": g0,
|
||||
"start_clearance_hits": clr0,
|
||||
"start_area": round(area0, 1),
|
||||
"end_crossings": int(best_meta["crossings"]),
|
||||
"end_clearance_hits": int(best_meta["clearance_hits"]),
|
||||
"end_area": best_meta["area"],
|
||||
"scale": best_meta["scale"],
|
||||
"moved_n": len(moved),
|
||||
"accepted": bool(best_meta["accepted"]),
|
||||
}
|
||||
st.meta["compact_bbox"] = meta
|
||||
return OpResult(
|
||||
state=st,
|
||||
moved=moved,
|
||||
op="compact_bbox",
|
||||
params=meta,
|
||||
note=(
|
||||
f"compact_bbox scale={meta['scale']} "
|
||||
f"Δx={meta['end_crossings'] - g0} "
|
||||
f"clr={clr0}→{meta['end_clearance_hits']} "
|
||||
f"area={meta['start_area']}→{meta['end_area']}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def compact_bbox_params_from_overrides(overrides: dict[str, Any] | None) -> dict[str, Any]:
|
||||
o = overrides or {}
|
||||
portals = o.get("portal_ids") or o.get("portals") or []
|
||||
if isinstance(portals, str):
|
||||
portals = [portals]
|
||||
frozen = o.get("frozen_ids") or []
|
||||
if isinstance(frozen, str):
|
||||
frozen = [frozen]
|
||||
outlier = o.get("outlier_only")
|
||||
if outlier is None:
|
||||
outlier_only = True
|
||||
else:
|
||||
outlier_only = str(outlier).strip().lower() not in {"0", "false", "no", "off"}
|
||||
return {
|
||||
"portal_ids": [str(x) for x in portals if str(x).strip()],
|
||||
"frozen_ids": {str(x) for x in frozen if str(x).strip()},
|
||||
"min_scale": float(o.get("min_scale") or 0.72),
|
||||
"step": float(o.get("step") or 0.03),
|
||||
"max_clearance_slack": int(o.get("max_clearance_slack") or 80),
|
||||
"outlier_only": outlier_only,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue