mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 00:23:19 +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
153
netx_api/mcp_remote/topology/layout_ops/scope.py
Normal file
153
netx_api/mcp_remote/topology/layout_ops/scope.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
"""Block / scope selection — ops act only on active nodes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable
|
||||
|
||||
from netx_api.mcp_remote.topology.layout_ops.graph_util import bbox, connected_components
|
||||
from netx_api.mcp_remote.topology.layout_ops.state import LayoutState, OpResult
|
||||
|
||||
|
||||
def active_nodes(state: LayoutState) -> set[str]:
|
||||
"""Nodes currently in scope (all if scope is None/empty meaning unrestricted)."""
|
||||
if state.scope is None:
|
||||
return set(state.positions.keys())
|
||||
return {n for n in state.scope if n in state.positions}
|
||||
|
||||
|
||||
def movable_nodes(state: LayoutState, *, respect_pins: bool = True) -> set[str]:
|
||||
"""Nodes an op may move: in scope, not hard agg/core, optionally not soft-pinned."""
|
||||
act = active_nodes(state)
|
||||
hard = {n for n in act if state.layers.get(n) in ("agg", "core")}
|
||||
out = act - hard
|
||||
if respect_pins:
|
||||
out -= set(state.pinned)
|
||||
return out
|
||||
|
||||
|
||||
def list_blocks(
|
||||
state: LayoutState,
|
||||
*,
|
||||
mode: str = "component",
|
||||
) -> list[set[str]]:
|
||||
"""Partition graph into blocks for per-block recipes.
|
||||
|
||||
Modes:
|
||||
- component: hard CC (default; existing recipes)
|
||||
- layer / bbox_quad: coarse buckets
|
||||
- hub_territory: soft blocks by hub-seeded BFS (our algo)
|
||||
- leiden / soft: hub territories + optional igraph Leiden on leftovers
|
||||
"""
|
||||
ids = set(state.positions.keys())
|
||||
m = (mode or "component").strip().lower()
|
||||
if m == "auto":
|
||||
from netx_api.mcp_remote.topology.layout_ops.partition import resolve_block_mode
|
||||
|
||||
m = resolve_block_mode(state, "auto")
|
||||
if m == "component":
|
||||
active = {
|
||||
n
|
||||
for n in ids
|
||||
if state.layers.get(n) in ("core", "agg", "access")
|
||||
}
|
||||
comps = connected_components(active, state.adj)
|
||||
blocks = [set(c) for c in comps if c]
|
||||
# orphans / other
|
||||
rest = ids - {n for b in blocks for n in b}
|
||||
if rest:
|
||||
blocks.append(rest)
|
||||
return blocks
|
||||
if m in {"hub_territory", "leiden", "soft"}:
|
||||
from netx_api.mcp_remote.topology.layout_ops.partition import (
|
||||
partition_soft_blocks,
|
||||
soft_blocks_as_sets,
|
||||
)
|
||||
|
||||
return soft_blocks_as_sets(partition_soft_blocks(state, mode=m))
|
||||
if m == "layer":
|
||||
by: dict[str, set[str]] = {}
|
||||
for n in ids:
|
||||
by.setdefault(state.layers.get(n, "other"), set()).add(n)
|
||||
return [by[k] for k in sorted(by.keys()) if by[k]]
|
||||
if m == "bbox_quad":
|
||||
if not ids:
|
||||
return []
|
||||
x0, y0, x1, y1 = bbox(state.positions)
|
||||
mx, my = (x0 + x1) / 2, (y0 + y1) / 2
|
||||
quads: list[set[str]] = [set() for _ in range(4)]
|
||||
for n, (x, y) in state.positions.items():
|
||||
qi = (0 if x < mx else 1) + (0 if y < my else 2)
|
||||
quads[qi].add(n)
|
||||
return [q for q in quads if q]
|
||||
# single full block
|
||||
return [ids] if ids else []
|
||||
|
||||
|
||||
def select_scope(
|
||||
state: LayoutState,
|
||||
*,
|
||||
mode: str = "all",
|
||||
node_ids: Iterable[str] | None = None,
|
||||
component_index: int | None = None,
|
||||
layer: str | None = None,
|
||||
bbox_region: tuple[float, float, float, float] | None = None,
|
||||
) -> OpResult:
|
||||
"""Set state.scope. mode=all clears scope (full graph)."""
|
||||
st = state.copy()
|
||||
pos = st.positions
|
||||
if mode == "all" or (mode == "ids" and not node_ids):
|
||||
st.scope = None
|
||||
return OpResult(
|
||||
state=st, moved=set(), op="select_scope", params={"mode": "all"}, note="scope=all"
|
||||
)
|
||||
|
||||
if mode == "ids" and node_ids is not None:
|
||||
scope = {n for n in node_ids if n in pos}
|
||||
elif mode == "component":
|
||||
blocks = list_blocks(st, mode="component")
|
||||
idx = 0 if component_index is None else int(component_index)
|
||||
scope = blocks[idx] if 0 <= idx < len(blocks) else set()
|
||||
elif mode == "layer":
|
||||
lyr = layer or "access"
|
||||
scope = {n for n in pos if st.layers.get(n) == lyr}
|
||||
elif mode == "bbox" and bbox_region is not None:
|
||||
x0, y0, x1, y1 = bbox_region
|
||||
scope = {
|
||||
n
|
||||
for n, (x, y) in pos.items()
|
||||
if x0 <= x <= x1 and y0 <= y <= y1
|
||||
}
|
||||
elif mode == "bbox_quad":
|
||||
blocks = list_blocks(st, mode="bbox_quad")
|
||||
idx = 0 if component_index is None else int(component_index)
|
||||
scope = blocks[idx] if 0 <= idx < len(blocks) else set()
|
||||
else:
|
||||
scope = set(pos.keys())
|
||||
|
||||
st.scope = scope
|
||||
return OpResult(
|
||||
state=st,
|
||||
moved=set(),
|
||||
op="select_scope",
|
||||
params={"mode": mode, "scope_n": len(scope), "component_index": component_index},
|
||||
note=f"scope={mode} n={len(scope)}",
|
||||
)
|
||||
|
||||
|
||||
def map_blocks(
|
||||
state: LayoutState,
|
||||
*,
|
||||
mode: str = "component",
|
||||
) -> OpResult:
|
||||
"""Record block partition in meta (for agents / foreach). Does not change scope."""
|
||||
st = state.copy()
|
||||
blocks = list_blocks(st, mode=mode)
|
||||
st.meta["blocks"] = [sorted(b) for b in blocks]
|
||||
st.meta["block_mode"] = mode
|
||||
return OpResult(
|
||||
state=st,
|
||||
moved=set(),
|
||||
op="map_blocks",
|
||||
params={"mode": mode, "blocks_n": len(blocks), "sizes": [len(b) for b in blocks]},
|
||||
note=f"blocks={len(blocks)} mode={mode}",
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue