mirror of
https://github.com/hansjone/netx.git
synced 2026-10-19 00:43:20 +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
92
netx_api/mcp_remote/topology/layout_job_worker.py
Normal file
92
netx_api/mcp_remote/topology/layout_job_worker.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
"""Subprocess entry for durable layout jobs (survives MCP parent restart)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(prog="netx_api.mcp_remote.topology.layout_job_worker")
|
||||
parser.add_argument("--job-id", required=True)
|
||||
args = parser.parse_args(argv)
|
||||
job_id = str(args.job_id).strip()
|
||||
if not job_id:
|
||||
return 2
|
||||
|
||||
from netx_api.mcp_remote.topology.layout_jobs import (
|
||||
_JobCancelled,
|
||||
_args_path,
|
||||
_read_job_disk,
|
||||
_write_job_disk,
|
||||
bind_job,
|
||||
finish_job,
|
||||
is_cancelled,
|
||||
report_progress,
|
||||
touch_heartbeat,
|
||||
unbind_job,
|
||||
)
|
||||
|
||||
args_file = _args_path(job_id)
|
||||
if not args_file.is_file():
|
||||
finish_job(job_id, error="job_args_missing")
|
||||
return 1
|
||||
|
||||
try:
|
||||
tool_args = json.loads(args_file.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
finish_job(job_id, error=f"job_args_invalid:{exc}")
|
||||
return 1
|
||||
|
||||
token = bind_job(job_id)
|
||||
import threading
|
||||
|
||||
stop_beat = threading.Event()
|
||||
|
||||
def _beat() -> None:
|
||||
while not stop_beat.wait(15.0):
|
||||
touch_heartbeat()
|
||||
|
||||
threading.Thread(target=_beat, name=f"job-beat-{job_id}", daemon=True).start()
|
||||
try:
|
||||
job = _read_job_disk(job_id) or {"job_id": job_id, "status": "running"}
|
||||
job["pid"] = __import__("os").getpid()
|
||||
job["heartbeat_at"] = time.time()
|
||||
job["progress"] = {
|
||||
**(job.get("progress") or {}),
|
||||
"phase": "running",
|
||||
"message": "worker bound",
|
||||
"updated_at": time.time(),
|
||||
"pct": 1.0,
|
||||
}
|
||||
_write_job_disk(job)
|
||||
report_progress("running", pct=1.0, message="worker started")
|
||||
if is_cancelled():
|
||||
raise _JobCancelled()
|
||||
|
||||
# Late import: pulls http_tools + layout stack.
|
||||
from netx_api.mcp_remote.topology.http_tools import _layout_topology_view
|
||||
|
||||
result = _layout_topology_view(tool_args)
|
||||
finish_job(job_id, result=result, cancelled=is_cancelled())
|
||||
return 0 if result.get("ok") else 1
|
||||
except _JobCancelled:
|
||||
finish_job(job_id, cancelled=True)
|
||||
return 0
|
||||
except Exception as exc: # noqa: BLE001
|
||||
finish_job(job_id, error=f"job_exception:{exc}")
|
||||
return 1
|
||||
finally:
|
||||
stop_beat.set()
|
||||
unbind_job(token)
|
||||
try:
|
||||
args_file.unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue