"""Minimal stdio MCP server exposing worai tools."""

from __future__ import annotations

import json
import os
import subprocess
import sys
import threading
import time
import uuid
from dataclasses import dataclass, field
import argparse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any

from typer.testing import CliRunner

from worai.config import resolve_config_path

_JSONRPC = "2.0"
_MAX_OUTPUT_CHARS_DEFAULT = 20000
_MAX_OUTPUT_CHARS_LIMIT = 200000
_SYNC_TIMEOUT_SEC = 120.0
_DEFAULT_EVENT_PREFIX = "WORAI_MCP_EVENT "
_EVENT_PREFIX_ENV = "WORAI_MCP_EVENT_PREFIX"
_FORBIDDEN_ARGV_PREFIXES: tuple[tuple[str, ...], ...] = (
    ("agent", "mcp", "serve"),
    ("self", "update", "--yes"),
)


@dataclass(frozen=True)
class SessionDefaults:
    config_path: str | None = None
    profile: str | None = None


@dataclass(frozen=True)
class ToolPolicy:
    mode: str  # sync | async_required


@dataclass
class Job:
    id: str
    command: list[str]
    effective_config_path: str
    effective_profile: str
    state: str = "queued"  # queued|running|done|failed|canceled
    created_at: float = field(default_factory=time.time)
    started_at: float | None = None
    finished_at: float | None = None
    exit_code: int | None = None
    output: str = ""
    events: list[dict[str, Any]] = field(default_factory=list)
    next_event_seq: int = 1
    progress: dict[str, Any] = field(default_factory=dict)
    process: subprocess.Popen[str] | None = None
    lock: threading.Lock = field(default_factory=threading.Lock)


class McpTransport:
    """JSON-RPC over stdio transport for MCP."""

    _line_delimited_mode: bool | None = None

    @staticmethod
    def read_message() -> dict[str, Any] | None:
        first_line = sys.stdin.buffer.readline()
        if not first_line:
            return None

        stripped = first_line.strip()
        if stripped.startswith((b"{", b"[")):
            McpTransport._line_delimited_mode = True
            data = json.loads(stripped.decode("utf-8"))
            if not isinstance(data, dict):
                return None
            return data

        headers: dict[str, str] = {}
        line = first_line
        while True:
            if line in {b"\r\n", b"\n"}:
                break
            decoded = line.decode("utf-8", errors="replace").strip()
            if ":" in decoded:
                key, value = decoded.split(":", 1)
                headers[key.strip().lower()] = value.strip()
            line = sys.stdin.buffer.readline()
            if not line:
                return None

        McpTransport._line_delimited_mode = False
        content_length_raw = headers.get("content-length")
        if not content_length_raw:
            return None
        content_length = int(content_length_raw)
        payload = sys.stdin.buffer.read(content_length)
        if not payload:
            return None
        data = json.loads(payload.decode("utf-8"))
        if not isinstance(data, dict):
            return None
        return data

    @staticmethod
    def write_message(message: dict[str, Any]) -> None:
        encoded = json.dumps(message).encode("utf-8")
        if McpTransport._line_delimited_mode is True:
            sys.stdout.buffer.write(encoded + b"\n")
        else:
            header = f"Content-Length: {len(encoded)}\r\n\r\n".encode("ascii")
            sys.stdout.buffer.write(header)
            sys.stdout.buffer.write(encoded)
        sys.stdout.buffer.flush()


class _PatchedEnvContext:
    def __init__(self, updates: dict[str, str]) -> None:
        self._updates = updates
        self._original: dict[str, str | None] = {}

    def __enter__(self) -> None:
        self._original = {key: os.environ.get(key) for key in self._updates}
        os.environ.update(self._updates)

    def __exit__(self, *_args: object) -> None:
        for key, value in self._original.items():
            if value is None:
                os.environ.pop(key, None)
            else:
                os.environ[key] = value


class WoraiExecutor:
    """Executes worai commands (sync in-process, async as subprocess command)."""

    def __init__(self, defaults: SessionDefaults, runner: CliRunner) -> None:
        self._defaults = defaults
        self._runner = runner

    @staticmethod
    def _root_app():
        from worai import cli as root_cli

        return root_cli.app

    def resolve_context(
        self,
        *,
        config_path: str | None,
        profile: str | None,
    ) -> tuple[str | None, str, str, list[str]]:
        effective_input_config = config_path or self._defaults.config_path
        effective_profile = profile or self._defaults.profile or os.environ.get("WORAI_PROFILE") or "default"
        resolved_config = str(resolve_config_path(effective_input_config, env=os.environ))

        root_argv: list[str] = []
        if effective_input_config:
            root_argv.extend(["--config", effective_input_config])
        if effective_profile:
            root_argv.extend(["--profile", effective_profile])
        return effective_input_config, effective_profile, resolved_config, root_argv

    def invoke_sync(
        self,
        *,
        argv: list[str],
        config_path: str | None,
        profile: str | None,
        max_output_chars: int,
    ) -> dict[str, Any]:
        _, effective_profile, resolved_config, root_argv = self.resolve_context(
            config_path=config_path,
            profile=profile,
        )
        full_argv = [*root_argv, *argv]
        with _PatchedEnvContext(
            {
                "WORAI_DISABLE_UPDATE_CHECK": "1",
                "WORAI_NO_PROGRESS": "1",
                "WORAI_PROGRESS": "0",
            }
        ):
            result = self._runner.invoke(self._root_app(), full_argv)
        output, truncated = _truncate(result.output, max_output_chars)
        return {
            "ok": result.exit_code == 0,
            "exit_code": int(result.exit_code),
            "command": ["worai", *full_argv],
            "effective_config_path": resolved_config,
            "effective_profile": effective_profile,
            "output": output,
            "output_truncated": truncated,
        }

    def build_subprocess_command(
        self,
        *,
        argv: list[str],
        config_path: str | None,
        profile: str | None,
    ) -> tuple[list[str], str, str]:
        _, effective_profile, resolved_config, root_argv = self.resolve_context(
            config_path=config_path,
            profile=profile,
        )
        cmd = [sys.executable, "-m", "worai.cli", *root_argv, *argv]
        return cmd, resolved_config, effective_profile


class WoraiJobManager:
    """Background job manager for async-required tools."""

    def __init__(self) -> None:
        self._jobs: dict[str, Job] = {}
        self._lock = threading.Lock()

    def start(self, *, command: list[str], effective_config_path: str, effective_profile: str) -> dict[str, Any]:
        job = Job(
            id=str(uuid.uuid4()),
            command=command,
            effective_config_path=effective_config_path,
            effective_profile=effective_profile,
        )
        with self._lock:
            self._jobs[job.id] = job

        thread = threading.Thread(target=self._run, args=(job.id,), daemon=True)
        thread.start()
        return {
            "job_id": job.id,
            "state": job.state,
            "effective_config_path": effective_config_path,
            "effective_profile": effective_profile,
            "command": command,
        }

    def _run(self, job_id: str) -> None:
        job = self._jobs[job_id]
        job.state = "running"
        job.started_at = time.time()
        self._append_event(job, "job.started", {"state": "running"})

        env = os.environ.copy()
        env["WORAI_DISABLE_UPDATE_CHECK"] = "1"
        env["WORAI_NO_PROGRESS"] = "1"
        env["WORAI_PROGRESS"] = "0"
        env["WORAI_MCP_PROGRESS"] = "1"
        env.setdefault(_EVENT_PREFIX_ENV, _DEFAULT_EVENT_PREFIX)

        proc = subprocess.Popen(
            job.command,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            bufsize=1,
            env=env,
        )
        job.process = proc

        assert proc.stdout is not None
        for line in proc.stdout:
            if self._consume_progress_event(job, line):
                continue
            with job.lock:
                job.output += line

        proc.wait()
        job.exit_code = int(proc.returncode)
        if job.state != "canceled":
            job.state = "done" if proc.returncode == 0 else "failed"
        self._append_event(job, "job.finished", {"state": job.state, "exit_code": job.exit_code})
        job.finished_at = time.time()

    def status(self, job_id: str) -> dict[str, Any]:
        job = self._get(job_id)
        with job.lock:
            progress = dict(job.progress)
            event_cursor = job.next_event_seq - 1
        return {
            "job_id": job.id,
            "state": job.state,
            "exit_code": job.exit_code,
            "created_at": job.created_at,
            "started_at": job.started_at,
            "finished_at": job.finished_at,
            "effective_config_path": job.effective_config_path,
            "effective_profile": job.effective_profile,
            "command": job.command,
            "progress": progress,
            "event_cursor": event_cursor,
        }

    def events(self, job_id: str, *, cursor: int = 0, limit: int = 100) -> dict[str, Any]:
        job = self._get(job_id)
        safe_cursor = max(0, int(cursor))
        safe_limit = max(1, min(int(limit), 500))
        with job.lock:
            events = [event for event in job.events if int(event.get("seq", 0)) > safe_cursor][:safe_limit]
            next_cursor = safe_cursor if not events else int(events[-1]["seq"])
        return {
            "job_id": job.id,
            "cursor": safe_cursor,
            "next_cursor": next_cursor,
            "events": events,
            "state": job.state,
        }

    def output(self, job_id: str, offset: int = 0, max_chars: int = _MAX_OUTPUT_CHARS_DEFAULT) -> dict[str, Any]:
        job = self._get(job_id)
        with job.lock:
            text = job.output
        safe_offset = max(0, min(offset, len(text)))
        chunk, truncated = _truncate(text[safe_offset:], max_chars)
        return {
            "job_id": job.id,
            "offset": safe_offset,
            "next_offset": safe_offset + len(chunk),
            "text": chunk,
            "truncated": truncated,
            "state": job.state,
        }

    def cancel(self, job_id: str) -> dict[str, Any]:
        job = self._get(job_id)
        if job.state in {"done", "failed", "canceled"}:
            return {"job_id": job.id, "state": job.state}
        proc = job.process
        if proc is not None:
            proc.terminate()
        job.state = "canceled"
        job.finished_at = time.time()
        self._append_event(job, "job.canceled", {"state": "canceled"})
        return {"job_id": job.id, "state": job.state}

    def _append_event(self, job: Job, event_name: str, meta: dict[str, Any]) -> None:
        now = time.time()
        with job.lock:
            event = {
                "seq": job.next_event_seq,
                "timestamp": now,
                "event": event_name,
                "meta": meta,
            }
            job.next_event_seq += 1
            job.events.append(event)
            if len(job.events) > 10000:
                job.events = job.events[-10000:]
            progress = self._progress_from_event(event_name, meta, now, dict(job.progress))
            if progress is not None:
                job.progress = progress

    def _consume_progress_event(self, job: Job, line: str) -> bool:
        prefix = os.environ.get(_EVENT_PREFIX_ENV, _DEFAULT_EVENT_PREFIX)
        if not line.startswith(prefix):
            return False
        raw = line[len(prefix):].strip()
        if not raw:
            return True
        try:
            payload = json.loads(raw)
        except json.JSONDecodeError:
            return True
        if not isinstance(payload, dict):
            return True
        event_name = str(payload.get("event", "")).strip()
        if not event_name:
            return True
        meta = payload.get("meta")
        safe_meta = meta if isinstance(meta, dict) else {}
        self._append_event(job, event_name, safe_meta)
        return True

    @staticmethod
    def _progress_from_event(
        event_name: str,
        meta: dict[str, Any],
        timestamp: float,
        current: dict[str, Any],
    ) -> dict[str, Any] | None:
        if event_name == "inventory.progress.started":
            total = int(meta.get("total", 0) or 0)
            return {
                "phase": "running",
                "completed": 0,
                "total": total,
                "percent": 0.0 if total > 0 else None,
                "last_update_at": timestamp,
                "last_event": event_name,
            }
        if event_name == "inventory.progress.updated":
            completed = int(meta.get("completed", 0) or 0)
            total = int(meta.get("total", completed) or completed)
            percent = None if total <= 0 else round((completed / total) * 100.0, 2)
            return {
                "phase": "running",
                "completed": completed,
                "total": total,
                "percent": percent,
                "last_update_at": timestamp,
                "last_event": event_name,
            }
        if event_name == "inventory.progress.completed":
            completed = int(meta.get("completed", current.get("completed", 0)) or 0)
            total = int(meta.get("total", current.get("total", completed)) or completed)
            percent = 100.0 if total > 0 else None
            return {
                "phase": "completed",
                "completed": completed,
                "total": total,
                "percent": percent,
                "last_update_at": timestamp,
                "last_event": event_name,
            }
        if event_name.startswith("job."):
            updated = dict(current)
            updated["last_update_at"] = timestamp
            updated["last_event"] = event_name
            return updated
        return None

    def _get(self, job_id: str) -> Job:
        with self._lock:
            job = self._jobs.get(job_id)
        if job is None:
            raise ValueError(f"Unknown job_id: {job_id}")
        return job


class WoraiMcpServer:
    """MCP server with typed tool dispatch and async jobs."""

    TOOL_POLICIES: dict[str, ToolPolicy] = {
        "worai_version": ToolPolicy(mode="sync"),
        "worai_help": ToolPolicy(mode="sync"),
        "worai_graph_audit": ToolPolicy(mode="sync"),
        "worai_graph_reset": ToolPolicy(mode="sync"),
        "worai_graph_validate": ToolPolicy(mode="sync"),
        "worai_graph_export": ToolPolicy(mode="sync"),
        "worai_entity_matrix": ToolPolicy(mode="sync"),
        "worai_structured_data_validate_page": ToolPolicy(mode="sync"),
        "worai_web_pages_classify_types": ToolPolicy(mode="sync"),
        "worai_delete_entities_from_csv": ToolPolicy(mode="sync"),
        "worai_canonicalize_duplicate_pages": ToolPolicy(mode="sync"),
        "worai_graph_property_delete": ToolPolicy(mode="sync"),
        "worai_cli_exec": ToolPolicy(mode="sync"),
        "worai_structured_data_inventory_start": ToolPolicy(mode="async_required"),
        "worai_graph_sync_run_start": ToolPolicy(mode="async_required"),
        "worai_job_status": ToolPolicy(mode="sync"),
        "worai_job_output": ToolPolicy(mode="sync"),
        "worai_job_events": ToolPolicy(mode="sync"),
        "worai_job_cancel": ToolPolicy(mode="sync"),
    }

    def __init__(self, defaults: SessionDefaults) -> None:
        self._defaults = defaults
        self._runner = CliRunner()
        self._executor = WoraiExecutor(defaults=defaults, runner=self._runner)
        self._jobs = WoraiJobManager()

    def serve(self) -> int:
        while True:
            message = McpTransport.read_message()
            if message is None:
                return 0
            response = self.handle_message(message)
            if response is not None:
                McpTransport.write_message(response)

    def handle_message(self, message: dict[str, Any]) -> dict[str, Any] | None:
        if "method" not in message:
            return None

        method = str(message["method"])
        request_id = message.get("id")
        params = message.get("params") or {}
        if not isinstance(params, dict):
            params = {}

        if request_id is None:
            if method == "notifications/initialized":
                return None
            return None
        try:
            result = self._dispatch(method, params)
            return {"jsonrpc": _JSONRPC, "id": request_id, "result": result}
        except Exception as exc:  # pragma: no cover - defensive branch
            return {
                "jsonrpc": _JSONRPC,
                "id": request_id,
                "error": {"code": -32000, "message": str(exc)},
            }

    def _dispatch(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
        if method == "initialize":
            return {
                "protocolVersion": "2024-11-05",
                "capabilities": {"tools": {}},
                "serverInfo": {"name": "worai-mcp", "version": "1.1.0"},
            }
        if method == "ping":
            return {}
        if method == "tools/list":
            return {"tools": self._tools()}
        if method == "tools/call":
            name = str(params.get("name", "")).strip()
            arguments = params.get("arguments") or {}
            if not isinstance(arguments, dict):
                arguments = {}
            payload = self._call_tool(name, arguments)
            return {
                "structuredContent": payload,
                "content": [{"type": "text", "text": json.dumps(payload)}],
                "isError": False,
            }
        raise ValueError(f"Method not found: {method}")

    def _tools(self) -> list[dict[str, Any]]:
        base_schema = {
            "config_path": {"type": "string"},
            "profile": {"type": "string"},
            "max_output_chars": {"type": "integer", "minimum": 256},
        }
        return [
            {
                "name": "worai_version",
                "description": "Show installed worai CLI version.",
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {"type": "object", "properties": base_schema, "additionalProperties": False},
            },
            {
                "name": "worai_help",
                "description": "Show worai --help for root or specific subcommand path.",
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        **base_schema,
                        "command_path": {"type": "array", "items": {"type": "string"}},
                    },
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_graph_audit",
                "description": (
                    "Run `worai graph audit` on a local graph file (TTL, JSON-LD, NT, RDF/XML). "
                    "Returns a schema compliance, rich-snippets, and connectivity report. "
                    "Exit code is non-zero when the file has load errors."
                ),
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        **base_schema,
                        "file": {"type": "string"},
                        "depth": {"type": "integer", "minimum": 0},
                        "list_isolated_iris": {"type": "boolean"},
                        "show_url_violations": {"type": "boolean"},
                        "rich_snippets_granularity": {"type": "string", "enum": ["counts", "entities"]},
                        "workers": {"type": "integer", "minimum": 1},
                        "builtin_shapes": {"type": "array", "items": {"type": "string"}},
                        "exclude_builtin_shapes": {"type": "array", "items": {"type": "string"}},
                        "shapes": {"type": "array", "items": {"type": "string"}},
                        "issue_level": {"type": "string", "enum": ["warning", "error"]},
                        "format": {"type": "string", "enum": ["text", "json"]},
                    },
                    "required": ["file"],
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_graph_reset",
                "description": (
                    "Run `worai graph reset` — resets the WordLift account associated with the "
                    "current profile. This operation is DESTRUCTIVE and IRREVERSIBLE. "
                    "Optionally keeps country, language, and/or URL settings."
                ),
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        **base_schema,
                        "keep_country": {"type": "boolean"},
                        "keep_language": {"type": "boolean"},
                        "keep_url": {"type": "boolean"},
                    },
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_graph_validate",
                "description": "Run `worai graph validate` against files/URLs.",
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        **base_schema,
                        "inputs": {"type": "array", "items": {"type": "string"}},
                        "builtin_shapes": {"type": "array", "items": {"type": "string"}},
                        "exclude_builtin_shapes": {"type": "array", "items": {"type": "string"}},
                        "shapes": {"type": "array", "items": {"type": "string"}},
                        "level": {"type": "string", "enum": ["warning", "error"]},
                        "format": {"type": "string", "enum": ["text", "json"]},
                    },
                    "required": ["inputs"],
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_graph_export",
                "description": "Run `worai graph export` with optional output file and --validate.",
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {**base_schema, "output_file": {"type": "string"}, "validate": {"type": "boolean"}},
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_entity_matrix",
                "description": (
                    "Run `worai entity-matrix` on a local TTL graph file. "
                    "Returns a URL × entity-type pivot table: one row per URL (or cluster), "
                    "one column per entity-type short name, cell values are integer counts. "
                    "Supports optional type exclusion and automatic URL clustering."
                ),
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        **base_schema,
                        "file": {"type": "string"},
                        "exclude_types": {"type": "array", "items": {"type": "string"}},
                        "cluster": {"type": "boolean"},
                        "depth": {"type": "integer", "minimum": 1},
                        "format": {
                            "type": "string",
                            "enum": ["table", "json", "csv", "tsv"],
                        },
                        "output_file": {"type": "string"},
                    },
                    "required": ["file"],
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_structured_data_validate_page",
                "description": "Run `worai structured-data validate page <url>`.",
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        **base_schema,
                        "url": {"type": "string"},
                        "shapes": {"type": "array", "items": {"type": "string"}},
                        "format": {"type": "string", "enum": ["pretty", "raw"]},
                        "color": {"type": "boolean"},
                        "headed": {"type": "boolean"},
                        "timeout_ms": {"type": "integer"},
                    },
                    "required": ["url"],
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_web_pages_classify_types",
                "description": "Run `worai web-pages classify-types` for a source and output CSV.",
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        **base_schema,
                        "source": {"type": "string"},
                        "output_csv": {"type": "string"},
                        "ingest_source": {"type": "string"},
                        "ingest_loader": {"type": "string"},
                        "url_regex": {"type": "string"},
                        "agent_cli": {"type": "string", "enum": ["codex", "claude", "gemini"]},
                        "agent_timeout_sec": {"type": "number"},
                        "max_markdown_chars": {"type": "integer"},
                    },
                    "required": ["source"],
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_delete_entities_from_csv",
                "description": (
                    "Run `worai delete-entities-from-csv` to delete entities whose IRIs are listed "
                    "in the first column of a CSV file. DESTRUCTIVE and IRREVERSIBLE."
                ),
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        **base_schema,
                        "csv_file": {"type": "string"},
                        "batch_size": {"type": "integer", "minimum": 1},
                    },
                    "required": ["csv_file"],
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_canonicalize_duplicate_pages",
                "description": (
                    "Run `worai canonicalize-duplicate-pages` — reads a GSC CSV produced by "
                    "`worai google-search-console`, queries the WordLift graph to find the "
                    "canonical target for each duplicate URL, and writes results to an output CSV."
                ),
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        **base_schema,
                        "input_csv": {"type": "string"},
                        "output_csv": {"type": "string"},
                        "url_regex": {"type": "string"},
                        "entity_type": {"type": "string"},
                        "endpoint": {"type": "string"},
                        "batch_size": {"type": "integer", "minimum": 1},
                        "kpi_window": {"type": "string", "enum": ["7d", "28d", "3m"]},
                        "kpi_metric": {"type": "string", "enum": ["clicks", "impressions", "ctr"]},
                    },
                    "required": ["input_csv"],
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_graph_property_delete",
                "description": (
                    "Run `worai graph property delete` — deletes one predicate from all matching "
                    "entities in the WordLift graph. DESTRUCTIVE. Use dry_run=true first to preview "
                    "matching entities without applying changes."
                ),
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        **base_schema,
                        "predicate": {"type": "string"},
                        "endpoint": {"type": "string"},
                        "entities_endpoint": {"type": "string"},
                        "dry_run": {"type": "boolean"},
                        "workers": {"type": "integer", "minimum": 1},
                        "retries": {"type": "integer", "minimum": 0},
                        "rate_delay": {"type": "number", "minimum": 0},
                        "limit": {"type": "integer", "minimum": 0},
                        "timeout": {"type": "integer", "minimum": 1},
                    },
                    "required": ["predicate"],
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_structured_data_inventory_start",
                "description": "Start `worai structured-data inventory` as a background job.",
                "annotations": {"x-execution-mode": "async_required"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        **base_schema,
                        "source": {"type": "string"},
                        "sheet_name": {"type": "string"},
                        "url_regex": {"type": "string"},
                        "source_type": {"type": "string"},
                        "ingest_source": {"type": "string"},
                        "ingest_loader": {"type": "string"},
                        "ingest_passthrough_when_html": {"type": "boolean"},
                        "output": {"type": "string"},
                        "destination_sheet_id": {"type": "string"},
                        "destination_sheet_name": {"type": "string"},
                        "client_secrets": {"type": "string"},
                        "token": {"type": "string"},
                        "port": {"type": "integer"},
                        "timeout": {"type": "number"},
                        "concurrency": {"type": "string"},
                    },
                    "required": ["source"],
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_graph_sync_run_start",
                "description": "Start `worai graph sync run` as a background job.",
                "annotations": {"x-execution-mode": "async_required"},
                "inputSchema": {
                    "type": "object",
                    "properties": {**base_schema, "debug": {"type": "boolean"}},
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_job_status",
                "description": "Get status for an async worai job.",
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {"job_id": {"type": "string"}},
                    "required": ["job_id"],
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_job_output",
                "description": "Read incremental output for an async worai job.",
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "job_id": {"type": "string"},
                        "offset": {"type": "integer", "minimum": 0},
                        "max_output_chars": {"type": "integer", "minimum": 256},
                    },
                    "required": ["job_id"],
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_job_events",
                "description": "Read structured progress events for an async worai job.",
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "job_id": {"type": "string"},
                        "cursor": {"type": "integer", "minimum": 0},
                        "limit": {"type": "integer", "minimum": 1},
                    },
                    "required": ["job_id"],
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_job_cancel",
                "description": "Cancel an async worai job.",
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {"job_id": {"type": "string"}},
                    "required": ["job_id"],
                    "additionalProperties": False,
                },
            },
            {
                "name": "worai_cli_exec",
                "description": "Fallback executor for uncovered commands. Accepts argv array only.",
                "annotations": {"x-execution-mode": "sync"},
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        **base_schema,
                        "argv": {"type": "array", "items": {"type": "string"}},
                    },
                    "required": ["argv"],
                    "additionalProperties": False,
                },
            },
        ]

    def _call_tool(self, name: str, arguments: dict[str, Any]) -> dict[str, Any]:
        policy = self.TOOL_POLICIES.get(name)
        if policy is None:
            raise ValueError(f"Unknown tool: {name}")

        if name == "worai_version":
            return self._invoke_worai_sync(
                argv=["version"],
                config_path=self._optional_str(arguments.get("config_path")),
                profile=self._optional_str(arguments.get("profile")),
                max_output_chars=self._max_output_chars(arguments.get("max_output_chars")),
            )
        if name == "worai_help":
            return self._tool_worai_help(arguments)
        if name == "worai_graph_audit":
            return self._tool_worai_graph_audit(arguments)
        if name == "worai_graph_reset":
            return self._tool_worai_graph_reset(arguments)
        if name == "worai_graph_validate":
            return self._tool_worai_graph_validate(arguments)
        if name == "worai_graph_export":
            return self._tool_worai_graph_export(arguments)
        if name == "worai_entity_matrix":
            return self._tool_worai_entity_matrix(arguments)
        if name == "worai_structured_data_validate_page":
            return self._tool_worai_structured_data_validate_page(arguments)
        if name == "worai_web_pages_classify_types":
            return self._tool_worai_web_pages_classify_types(arguments)
        if name == "worai_delete_entities_from_csv":
            return self._tool_worai_delete_entities_from_csv(arguments)
        if name == "worai_canonicalize_duplicate_pages":
            return self._tool_worai_canonicalize_duplicate_pages(arguments)
        if name == "worai_graph_property_delete":
            return self._tool_worai_graph_property_delete(arguments)
        if name == "worai_cli_exec":
            return self._tool_worai_cli_exec(arguments)
        if name == "worai_structured_data_inventory_start":
            return self._tool_worai_structured_data_inventory_start(arguments)
        if name == "worai_graph_sync_run_start":
            return self._tool_worai_graph_sync_run_start(arguments)
        if name == "worai_job_status":
            job_id = self._required_str(arguments.get("job_id"), field_name="job_id")
            return self._jobs.status(job_id)
        if name == "worai_job_output":
            job_id = self._required_str(arguments.get("job_id"), field_name="job_id")
            offset = int(arguments.get("offset", 0) or 0)
            max_chars = self._max_output_chars(arguments.get("max_output_chars"))
            return self._jobs.output(job_id, offset=offset, max_chars=max_chars)
        if name == "worai_job_events":
            job_id = self._required_str(arguments.get("job_id"), field_name="job_id")
            cursor = int(arguments.get("cursor", 0) or 0)
            limit = int(arguments.get("limit", 100) or 100)
            return self._jobs.events(job_id, cursor=cursor, limit=limit)
        if name == "worai_job_cancel":
            job_id = self._required_str(arguments.get("job_id"), field_name="job_id")
            return self._jobs.cancel(job_id)
        raise ValueError(f"Unknown tool: {name}")

    def _invoke_worai_sync(
        self,
        *,
        argv: list[str],
        config_path: str | None,
        profile: str | None,
        max_output_chars: int,
    ) -> dict[str, Any]:
        started = time.time()
        result = self._executor.invoke_sync(
            argv=argv,
            config_path=config_path,
            profile=profile,
            max_output_chars=max_output_chars,
        )
        elapsed = time.time() - started
        if elapsed > _SYNC_TIMEOUT_SEC:
            raise ValueError("Sync execution exceeded threshold. Use async *_start tool.")
        return result

    def _tool_worai_help(self, arguments: dict[str, Any]) -> dict[str, Any]:
        raw_command_path = arguments.get("command_path") or []
        if raw_command_path and (
            not isinstance(raw_command_path, list)
            or not all(isinstance(item, str) and item.strip() for item in raw_command_path)
        ):
            raise ValueError("command_path must be an array of non-empty strings")
        argv = [*raw_command_path, "--help"]
        return self._invoke_worai_sync(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
            max_output_chars=self._max_output_chars(arguments.get("max_output_chars")),
        )

    def _tool_worai_delete_entities_from_csv(self, arguments: dict[str, Any]) -> dict[str, Any]:
        csv_file = self._required_str(arguments.get("csv_file"), field_name="csv_file")
        argv: list[str] = ["delete-entities-from-csv", csv_file]
        batch_size = arguments.get("batch_size")
        if batch_size is not None:
            argv.extend(["--batch-size", str(int(batch_size))])
        return self._invoke_worai_sync(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
            max_output_chars=self._max_output_chars(arguments.get("max_output_chars")),
        )

    def _tool_worai_canonicalize_duplicate_pages(self, arguments: dict[str, Any]) -> dict[str, Any]:
        input_csv = self._required_str(arguments.get("input_csv"), field_name="input_csv")
        argv: list[str] = ["canonicalize-duplicate-pages", "--input", input_csv]
        output_csv = self._optional_str(arguments.get("output_csv"))
        if output_csv:
            argv.extend(["--output", output_csv])
        url_regex = self._optional_str(arguments.get("url_regex"))
        if url_regex:
            argv.extend(["--url-regex", url_regex])
        entity_type = self._optional_str(arguments.get("entity_type"))
        if entity_type:
            argv.extend(["--entity-type", entity_type])
        endpoint = self._optional_str(arguments.get("endpoint"))
        if endpoint:
            argv.extend(["--endpoint", endpoint])
        batch_size = arguments.get("batch_size")
        if batch_size is not None:
            argv.extend(["--batch-size", str(int(batch_size))])
        kpi_window = self._optional_str(arguments.get("kpi_window"))
        if kpi_window:
            argv.extend(["--kpi-window", kpi_window])
        kpi_metric = self._optional_str(arguments.get("kpi_metric"))
        if kpi_metric:
            argv.extend(["--kpi-metric", kpi_metric])
        return self._invoke_worai_sync(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
            max_output_chars=self._max_output_chars(arguments.get("max_output_chars")),
        )

    def _tool_worai_graph_property_delete(self, arguments: dict[str, Any]) -> dict[str, Any]:
        predicate = self._required_str(arguments.get("predicate"), field_name="predicate")
        argv: list[str] = ["graph", "property", "delete", predicate, "--yes"]
        if bool(arguments.get("dry_run")):
            argv.append("--dry-run")
        endpoint = self._optional_str(arguments.get("endpoint"))
        if endpoint:
            argv.extend(["--endpoint", endpoint])
        entities_endpoint = self._optional_str(arguments.get("entities_endpoint"))
        if entities_endpoint:
            argv.extend(["--entities-endpoint", entities_endpoint])
        workers = arguments.get("workers")
        if workers is not None:
            argv.extend(["--workers", str(int(workers))])
        retries = arguments.get("retries")
        if retries is not None:
            argv.extend(["--retries", str(int(retries))])
        rate_delay = arguments.get("rate_delay")
        if rate_delay is not None:
            argv.extend(["--rate-delay", str(float(rate_delay))])
        limit = arguments.get("limit")
        if limit is not None:
            argv.extend(["--limit", str(int(limit))])
        timeout = arguments.get("timeout")
        if timeout is not None:
            argv.extend(["--timeout", str(int(timeout))])
        return self._invoke_worai_sync(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
            max_output_chars=self._max_output_chars(arguments.get("max_output_chars")),
        )

    def _tool_worai_cli_exec(self, arguments: dict[str, Any]) -> dict[str, Any]:
        raw_argv = arguments.get("argv")
        if not isinstance(raw_argv, list) or not raw_argv:
            raise ValueError("argv must be a non-empty array of strings")
        if not all(isinstance(item, str) and item.strip() for item in raw_argv):
            raise ValueError("argv must contain only non-empty strings")
        argv = [item.strip() for item in raw_argv]
        for blocked in _FORBIDDEN_ARGV_PREFIXES:
            if tuple(argv[: len(blocked)]) == blocked:
                raise ValueError(f"Command is blocked: {' '.join(blocked)}")
        return self._invoke_worai_sync(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
            max_output_chars=self._max_output_chars(arguments.get("max_output_chars")),
        )

    def _tool_worai_graph_audit(self, arguments: dict[str, Any]) -> dict[str, Any]:
        file = self._required_str(arguments.get("file"), field_name="file")
        argv: list[str] = ["graph", "audit", file]
        depth = arguments.get("depth")
        if depth is not None:
            argv.extend(["--depth", str(int(depth))])
        if bool(arguments.get("list_isolated_iris")):
            argv.append("--list-isolated-iris")
        if bool(arguments.get("show_url_violations")):
            argv.append("--show-url-violations")
        granularity = self._optional_str(arguments.get("rich_snippets_granularity"))
        if granularity:
            argv.extend(["--rich-snippets-granularity", granularity])
        workers = arguments.get("workers")
        if workers is not None:
            argv.extend(["--workers", str(int(workers))])
        for shape in self._string_list(arguments.get("builtin_shapes"), field_name="builtin_shapes"):
            argv.extend(["--builtin-shape", shape])
        for shape in self._string_list(arguments.get("exclude_builtin_shapes"), field_name="exclude_builtin_shapes"):
            argv.extend(["--exclude-builtin-shape", shape])
        for shape in self._string_list(arguments.get("shapes"), field_name="shapes"):
            argv.extend(["--shape", shape])
        issue_level = self._optional_str(arguments.get("issue_level"))
        if issue_level:
            argv.extend(["--issue-level", issue_level])
        output_format = self._optional_str(arguments.get("format"))
        if output_format:
            argv.extend(["--format", output_format])
        return self._invoke_worai_sync(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
            max_output_chars=self._max_output_chars(arguments.get("max_output_chars")),
        )

    def _tool_worai_graph_reset(self, arguments: dict[str, Any]) -> dict[str, Any]:
        argv: list[str] = ["graph", "reset", "--yes"]
        if arguments.get("keep_country"):
            argv.append("--keep-country")
        if arguments.get("keep_language"):
            argv.append("--keep-language")
        if arguments.get("keep_url"):
            argv.append("--keep-url")
        return self._invoke_worai_sync(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
            max_output_chars=self._max_output_chars(arguments.get("max_output_chars")),
        )

    def _tool_worai_graph_validate(self, arguments: dict[str, Any]) -> dict[str, Any]:
        inputs = self._string_list(arguments.get("inputs"), field_name="inputs", required=True)
        argv: list[str] = ["graph", "validate", *inputs]
        for shape in self._string_list(arguments.get("builtin_shapes"), field_name="builtin_shapes"):
            argv.extend(["--builtin-shape", shape])
        for shape in self._string_list(arguments.get("exclude_builtin_shapes"), field_name="exclude_builtin_shapes"):
            argv.extend(["--exclude-builtin-shape", shape])
        for shape in self._string_list(arguments.get("shapes"), field_name="shapes"):
            argv.extend(["--shape", shape])
        level = self._optional_str(arguments.get("level"))
        if level:
            argv.extend(["--level", level])
        output_format = self._optional_str(arguments.get("format"))
        if output_format:
            argv.extend(["--format", output_format])
        return self._invoke_worai_sync(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
            max_output_chars=self._max_output_chars(arguments.get("max_output_chars")),
        )

    def _tool_worai_graph_export(self, arguments: dict[str, Any]) -> dict[str, Any]:
        argv: list[str] = ["graph", "export"]
        output_file = self._optional_str(arguments.get("output_file"))
        if output_file:
            argv.append(output_file)
        if bool(arguments.get("validate")):
            argv.append("--validate")
        return self._invoke_worai_sync(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
            max_output_chars=self._max_output_chars(arguments.get("max_output_chars")),
        )

    def _tool_worai_entity_matrix(self, arguments: dict[str, Any]) -> dict[str, Any]:
        file = self._required_str(arguments.get("file"), field_name="file")
        argv: list[str] = ["entity-matrix", file]
        for t in self._string_list(arguments.get("exclude_types"), field_name="exclude_types"):
            argv.extend(["--exclude-type", t])
        cluster = arguments.get("cluster")
        if cluster is True:
            argv.append("--cluster")
        elif cluster is False:
            argv.append("--no-cluster")
        depth = arguments.get("depth")
        if depth is not None:
            argv.extend(["--depth", str(int(depth))])
        output_format = self._optional_str(arguments.get("format"))
        if output_format:
            argv.extend(["--format", output_format])
        output_file = self._optional_str(arguments.get("output_file"))
        if output_file:
            argv.extend(["--output", output_file])
        return self._invoke_worai_sync(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
            max_output_chars=self._max_output_chars(arguments.get("max_output_chars")),
        )

    def _tool_worai_structured_data_validate_page(self, arguments: dict[str, Any]) -> dict[str, Any]:
        url = self._required_str(arguments.get("url"), field_name="url")
        argv: list[str] = ["structured-data", "validate", "page", url]
        for shape in self._string_list(arguments.get("shapes"), field_name="shapes"):
            argv.extend(["--shape", shape])
        output_format = self._optional_str(arguments.get("format"))
        if output_format:
            argv.extend(["--format", output_format])
        color = arguments.get("color")
        if color is True:
            argv.append("--color")
        if color is False:
            argv.append("--no-color")
        if bool(arguments.get("headed")):
            argv.append("--headed")
        timeout_ms = arguments.get("timeout_ms")
        if timeout_ms is not None:
            argv.extend(["--timeout-ms", str(int(timeout_ms))])
        return self._invoke_worai_sync(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
            max_output_chars=self._max_output_chars(arguments.get("max_output_chars")),
        )

    def _tool_worai_web_pages_classify_types(self, arguments: dict[str, Any]) -> dict[str, Any]:
        source = self._required_str(arguments.get("source"), field_name="source")
        argv: list[str] = ["web-pages", "classify-types", source, "--yes"]
        output_csv = self._optional_str(arguments.get("output_csv"))
        if output_csv:
            argv.extend(["--output", output_csv])
        ingest_source = self._optional_str(arguments.get("ingest_source"))
        if ingest_source:
            argv.extend(["--ingest-source", ingest_source])
        ingest_loader = self._optional_str(arguments.get("ingest_loader"))
        if ingest_loader:
            argv.extend(["--ingest-loader", ingest_loader])
        url_regex = self._optional_str(arguments.get("url_regex"))
        if url_regex:
            argv.extend(["--url-regex", url_regex])
        agent_cli = self._optional_str(arguments.get("agent_cli"))
        if agent_cli:
            argv.extend(["--agent-cli", agent_cli])
        timeout = arguments.get("agent_timeout_sec")
        if timeout is not None:
            argv.extend(["--agent-timeout-sec", str(float(timeout))])
        max_markdown = arguments.get("max_markdown_chars")
        if max_markdown is not None:
            argv.extend(["--max-markdown-chars", str(int(max_markdown))])
        return self._invoke_worai_sync(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
            max_output_chars=self._max_output_chars(arguments.get("max_output_chars")),
        )

    def _tool_worai_structured_data_inventory_start(self, arguments: dict[str, Any]) -> dict[str, Any]:
        source = self._required_str(arguments.get("source"), field_name="source")
        argv: list[str] = ["structured-data", "inventory", source]
        for arg_name, option_name in (
            ("sheet_name", "--sheet-name"),
            ("url_regex", "--url-regex"),
            ("source_type", "--source-type"),
            ("ingest_source", "--ingest-source"),
            ("ingest_loader", "--ingest-loader"),
            ("output", "--output"),
            ("destination_sheet_id", "--destination-sheet-id"),
            ("destination_sheet_name", "--destination-sheet-name"),
            ("client_secrets", "--client-secrets"),
            ("token", "--token"),
            ("concurrency", "--concurrency"),
        ):
            value = self._optional_str(arguments.get(arg_name))
            if value:
                argv.extend([option_name, value])

        passthrough = arguments.get("ingest_passthrough_when_html")
        if passthrough is True:
            argv.append("--ingest-passthrough-when-html")
        if passthrough is False:
            argv.append("--no-ingest-passthrough-when-html")

        timeout = arguments.get("timeout")
        if timeout is not None:
            argv.extend(["--timeout", str(float(timeout))])
        port = arguments.get("port")
        if port is not None:
            argv.extend(["--port", str(int(port))])

        command, resolved_config, effective_profile = self._executor.build_subprocess_command(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
        )
        return self._jobs.start(
            command=command,
            effective_config_path=resolved_config,
            effective_profile=effective_profile,
        )

    def _tool_worai_graph_sync_run_start(self, arguments: dict[str, Any]) -> dict[str, Any]:
        argv: list[str] = ["graph", "sync", "run"]
        if bool(arguments.get("debug")):
            argv.append("--debug")
        command, resolved_config, effective_profile = self._executor.build_subprocess_command(
            argv=argv,
            config_path=self._optional_str(arguments.get("config_path")),
            profile=self._optional_str(arguments.get("profile")),
        )
        return self._jobs.start(
            command=command,
            effective_config_path=resolved_config,
            effective_profile=effective_profile,
        )

    @staticmethod
    def _optional_str(value: Any) -> str | None:
        if value is None:
            return None
        if not isinstance(value, str):
            raise ValueError("Expected a string value")
        stripped = value.strip()
        return stripped or None

    @staticmethod
    def _required_str(value: Any, *, field_name: str) -> str:
        optional = WoraiMcpServer._optional_str(value)
        if not optional:
            raise ValueError(f"{field_name} is required")
        return optional

    @staticmethod
    def _string_list(value: Any, *, field_name: str, required: bool = False) -> list[str]:
        if value is None:
            if required:
                raise ValueError(f"{field_name} is required")
            return []
        if not isinstance(value, list):
            raise ValueError(f"{field_name} must be an array of non-empty strings")
        if not all(isinstance(item, str) and item.strip() for item in value):
            raise ValueError(f"{field_name} must be an array of non-empty strings")
        return [item.strip() for item in value]

    @staticmethod
    def _max_output_chars(value: Any) -> int:
        if value is None:
            return _MAX_OUTPUT_CHARS_DEFAULT
        try:
            parsed = int(value)
        except (TypeError, ValueError) as exc:
            raise ValueError("max_output_chars must be an integer") from exc
        return max(256, min(parsed, _MAX_OUTPUT_CHARS_LIMIT))


def _truncate(text: str, max_chars: int) -> tuple[str, bool]:
    if len(text) <= max_chars:
        return text, False
    return text[:max_chars], True


def run_stdio_server(*, config_path: str | None, profile: str | None) -> int:
    defaults = SessionDefaults(config_path=config_path, profile=profile)
    return WoraiMcpServer(defaults=defaults).serve()


@dataclass
class RunningHttpServer:
    server: ThreadingHTTPServer
    thread: threading.Thread
    url: str

    def stop(self) -> None:
        self.server.shutdown()
        self.server.server_close()
        self.thread.join(timeout=2.0)


class _HttpMcpHandler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    @property
    def _mcp(self) -> WoraiMcpServer:
        return self.server.mcp_server  # type: ignore[attr-defined]

    def _send_json(self, status: int, payload: dict[str, Any]) -> None:
        encoded = json.dumps(payload).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(encoded)))
        self.end_headers()
        self.wfile.write(encoded)

    def do_GET(self) -> None:  # noqa: N802
        if self.path == "/health":
            self._send_json(200, {"ok": True})
            return
        self.send_response(404)
        self.send_header("Content-Length", "0")
        self.end_headers()

    def do_POST(self) -> None:  # noqa: N802
        if self.path != "/mcp":
            self.send_response(404)
            self.send_header("Content-Length", "0")
            self.end_headers()
            return
        try:
            content_length = int(self.headers.get("Content-Length", "0"))
        except ValueError:
            self._send_json(400, {"error": "invalid content length"})
            return
        body = self.rfile.read(content_length) if content_length > 0 else b""
        try:
            data = json.loads(body.decode("utf-8"))
        except json.JSONDecodeError:
            self._send_json(400, {"error": "invalid json"})
            return
        if not isinstance(data, dict):
            self._send_json(400, {"error": "json-rpc object required"})
            return
        response = self._mcp.handle_message(data)
        if response is None:
            self.send_response(202)
            self.send_header("Content-Length", "0")
            self.end_headers()
            return
        self._send_json(200, response)

    def log_message(self, format: str, *args: object) -> None:  # noqa: A003
        return


def start_http_server(
    *,
    host: str,
    port: int,
    config_path: str | None,
    profile: str | None,
) -> RunningHttpServer:
    defaults = SessionDefaults(config_path=config_path, profile=profile)
    mcp_server = WoraiMcpServer(defaults=defaults)
    http = ThreadingHTTPServer((host, int(port)), _HttpMcpHandler)
    http.mcp_server = mcp_server  # type: ignore[attr-defined]
    thread = threading.Thread(target=http.serve_forever, daemon=True)
    thread.start()
    actual_port = int(http.server_address[1])
    return RunningHttpServer(
        server=http,
        thread=thread,
        url=f"http://{host}:{actual_port}/mcp",
    )


def _parse_args(argv: list[str]) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Run worai MCP server over stdio.")
    parser.add_argument("--config", dest="config_path", default=None)
    parser.add_argument("--profile", dest="profile", default=None)
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    args = _parse_args(argv or sys.argv[1:])
    resolved_config = str(resolve_config_path(args.config_path, env=os.environ)) if args.config_path else None
    return run_stdio_server(config_path=resolved_config, profile=args.profile)


if __name__ == "__main__":
    raise SystemExit(main())
