from __future__ import annotations

import argparse
import concurrent.futures
import datetime
import http.client
import json
import os
import queue
import sys
import threading
import time
import traceback
import hashlib
import urllib.error
import urllib.parse
import urllib.request
import uuid
import re
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any

THIS_FILE = Path(__file__).resolve()
ROOT_DIR = THIS_FILE.parents[3] if len(THIS_FILE.parents) > 3 else THIS_FILE.parent
if str(ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(ROOT_DIR))

DEFAULT_ADAPTER_URL = "http://docker.cin.su:8011"
DEFAULT_ACCESS_REPORT_ROOT = ROOT_DIR / "reports" / "1c-access"
PROTOCOL_VERSION = "2025-06-18"
MCP_CONTRACT_VERSION = "onec-selector-contract.v1"

SESSIONS: dict[str, "queue.Queue[dict[str, Any] | None]"] = {}
SESSION_LOCK = threading.Lock()
JOB_LOCK = threading.Lock()
MCP_AUDIT_LOCK = threading.Lock()
SELECTOR_TOKEN_LOCK = threading.Lock()
JOBS: dict[str, dict[str, Any]] = {}
NEW_METHOD_CACHE: dict[str, dict[str, Any]] = {}
SELECTOR_TOKENS: dict[str, dict[str, Any]] = {}
SELECTOR_TOKEN_TTL_SECONDS = 600
LONG_METHODS = {
    "metadata.object.attributes",
    "metadata.object.full",
    "metadata.objects.list",
    "metadata.form.decode",
    "metadata.definition.find",
    "metadata.resolve_overrides",
    "modules.search",
    "code.search",
    "code.read",
    "templates.bindings",
    "templates.read",
    "templates.analyze",
    "templates.map",
    "extension.objects.find",
    "metadata.route.resolve",
    "diagnostics.call_chain",
    "bulk.execute",
}
DIAGNOSTIC_METHOD_PREFIXES = (
    "codec.",
    "storage.",
    "schema.",
    "query.",
)
DIAGNOSTIC_METHODS = {
    "metadata.dbnames.summary",
}
BASE_ID_REQUIRED_METHOD_PREFIXES = (
    "infobase.",
    "metadata.",
    "modules.",
    "code.",
    "templates.",
    "extension.",
    "diagnostics.",
    "extensions.",
    "access.",
    "query.",
    "storage.",
    "schema.",
    "codec.",
)
BASE_ID_OPTIONAL_METHODS = {
    "health",
    "help.methods",
    "adapter.job.start",
    "adapter.job.get",
    "adapter.job.cancel",
}
HEAVY_TEXT_THRESHOLD_FOR_CODE_READ = 50000
OWNER_IDENTITY_LOOKUP_LIMIT = 24
NEW_API_VERSION = 1
UNIFIED_METHODS = {
    "bulk.execute",
}
NEW_METHOD_CACHE_TTL_SECONDS = {"short": 30, "normal": 120, "long": 600}
BULK_MAX_REQUESTS = 30
SOURCE_MODES = {"auto", "runtime", "designer"}
OBJECT_SELECTOR_SCHEMA_PROPERTIES = {
    "ref": {"type": "string"},
    "kind": {"type": "string"},
    "name": {"type": "string"},
    "guid": {"type": "string"},
    "object_type": {"type": "string"},
    "object_name": {"type": "string"},
    "object_guid": {"type": "string"},
}
CACHE_POLICIES = {"none", "ttl", "snapshot", "stale_while_revalidate"}
SOURCE_STATES = {"applied", "working", "all"}
CONFIGURATION_VIEW_TO_SOURCE_STATE = {
    "effective": "working",
    "effective_working": "working",
    "designer": "working",
    "working": "working",
    "runtime": "applied",
    "runtime_applied": "applied",
    "applied": "applied",
    "compare": "all",
    "comparison": "all",
    "both": "all",
}
REST_STATE_BY_SOURCE_STATE = {
    "applied": "active",
    "working": "working",
    "all": "both",
}
REST_STATE_METHODS = {
    "metadata.object.forms",
    "metadata.object.form.details",
    "metadata.form.decode",
    "metadata.object.full",
    "metadata.resolve_overrides",
    "modules.search",
    "code.search",
    "code.read",
    "extension.objects.find",
}
DEFAULT_SOURCE_MODE = "auto"
DEFAULT_CACHE_POLICY_BY_SOURCE_MODE = {
    "runtime": "ttl",
    "designer": "none",
    "auto": "ttl",
}
BSL_TEMPLATE_BINDING_RE = re.compile(r"&([^&;\n\r]+)&|\{([A-Za-zА-Яа-я0-9_\-.]+)\}|\%\%([A-Za-zА-Яа-я0-9_\-.]+)\%\%")
ROUTINE_RESERVE_WORDS = {
    "и", "или", "не", "если", "иначе", "иначеесли", "конец", "для", "все", "иначе", "процедура", "функция",
    "конецпроцедуры", "конецфункции", "цикл", "пока", "по", "тогда", "возврат", "прервать", "продолжить",
    "попытка", "исключение", "return", "and", "or", "true", "false", "undefined", "null",
}
SYSTEM_CALL_HINT_WORDS = {
    "выполнить", "выполнитьинтерфейс", "выполнитьоператор", "получитьколичество", "новый", "new",
    "and", "or", "not", "если", "иначе", "конец", "пока", "for", "while", "foreach", "do", "then", "else",
    "true", "false", "null", "undefined", "return", "прервать", "продолжить",
}

GUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
FULL_METHOD_SECTIONS = {"card", "semantic", "forms", "templates", "commands", "modules", "parts_summary"}
FULL_METHOD_SECTION_ORDER = ["card", "semantic", "modules", "templates", "forms", "commands"]
FULL_METHOD_ALL_KEY = "all"
TECHNICAL_AGENT_FIELDS = {
    "table", "file_name", "file_names", "module_ref", "module_id",
    "stream_index", "bsl_offset", "cas_key", "storage_key",
    "include_storage", "guid", "object_guid", "form_guid", "extension_guid",
}


TOOLS = [
    {
        "name": "onec_health",
        "description": "Check the 1C adapter MCP proxy and the configured REST adapter endpoint.",
        "inputSchema": {
            "type": "object",
            "properties": {
                "base_id": {
                    "type": "string",
                    "description": "Optional concrete 1C base id to check. The adapter does not use a default database.",
                }
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "onec_help",
        "description": "Return available 1C adapter methods from the REST adapter.",
        "inputSchema": {
            "type": "object",
            "properties": {
                "method": {
                    "type": "string",
                    "description": "Optional method name to inspect.",
                }
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "onec_request",
        "description": (
            "Generic 1C adapter request. For live metadata/modules/code/templates/extensions/query methods, "
            "payload.base_id is required; get it from user/project context or check a concrete base with onec_health first. "
            "Use complete public 1C names first: extension + object ref + child name where applicable. "
            "Search results declare read_selector.method and include read_selector.selector_token; reuse that token with its declared method for the next read call. "
            "For a name search across metadata objects, forms, attributes, commands, templates, routines, and extension definitions, use metadata.definition.find; "
            "use code.search only when the query is BSL text. Scope extension objects with extension.objects.find or metadata.definition.find areas=extensions, never by SQL table names. "
            "For unresolved module owners, inspect diagnostics.owner_resolution and adjust the public object selector "
            "(ref, kind/name, or object_type/object_name). Global code/vector searches resolve "
            "base module owners lazily from current metadata; use metadata.module_owner_cache.backfill for bounded "
            "background warming instead of increasing owner_scan_limit on interactive searches. "
            "The default agent view is configuration_view=effective_working with source_state=working: the logical Designer snapshot, with working changes and extension layers preferred; "
            "it becomes executable after configuration update, not necessarily now. Use configuration_view=runtime_applied for code executable now, or compare to inspect both. "
            "Do not select Config/ConfigSave tables in ordinary programming calls. "
            "For saved-state methods, select base_saved_state or extension_saved_state with layer and identify objects by ref or kind/name. "
            "Do not send GUIDs, table/file_name/module_ref, stream indexes, CAS keys, or include_storage in ordinary agent requests. "
            "Before writes, call metadata.write.preflight when you need a read-only route/freshness check; it reports "
            "ready, needs_prepare, needs_resolution, or blocked and never applies SQL writes. "
            "Repository manual-capture protocol: when repository.lock.request or repository.lock.request.status returns "
            "status=pending_user_lock, do not invent confirmation fields. After the user confirms the exact object is "
            "captured in Configurator, call the returned next_call.method using next_call.params as this tool's payload. "
            "The successful confirmation returns write_context; forward it unchanged as payload.repository_lock (or copy its fields to the payload top level) for write preflight and write calls. "
            "For BSL edits, prefer high-level metadata.write: pass a 1C canonical path, routine_text, and routine_operation; "
            "the adapter prepares saved-state when needed, saves into working/saved-state metadata, and never activates it. "
            "For a scheduled-job schedule, use metadata.write with target.kind=schedule and target.ref such as РегламентныеЗадания.ОбменДанными; pass named schedule fields instead of GUIDs or tree paths. "
            "For adding a form command with a visible button and handler routine, use metadata.form.command_button.write. "
            "Use code.write only as a compatibility shortcut for simple module edits. "
            "Long metadata calls return a job_id quickly; poll it with method mcp.job.get."
        ),
        "inputSchema": {
            "type": "object",
            "required": ["method"],
            "examples": [
                {
                    "method": "metadata.resolve_overrides",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "object_type": "<metadata-kind>",
                        "object_name": "<metadata-object-name>",
                        "method_name": "<routine-name>",
                        "source_state": "working",
                    },
                },
                {
                    "method": "metadata.objects.list",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "kind": "<metadata-kind>",
                        "limit": 50,
                        "offset": 0,
                    },
                },
                {
                    "method": "metadata.object.full",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "ref": "<metadata-kind>.<metadata-object-name>",
                        "sections": ["modules", "templates", "forms"],
                    },
                },
                {
                    "method": "extension.objects.find",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "extension": "<extension-name>",
                        "object_type": "CommonForm",
                        "source_state": "working",
                        "limit": 50,
                    },
                },
                {
                    "method": "code.search",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "query": "<text-or-routine-fragment>",
                        "object_type": "<optional-metadata-kind>",
                        "object_name": "<optional-metadata-object-name>",
                        "source_state": "working",
                        "scan_limit": 400,
                    },
                },
                {
                    "method": "code.write",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "object_type": "<metadata-kind>",
                        "object_name": "<metadata-object-name>",
                        "routine_name": "<routine-name>",
                        "routine_text": "<full-procedure-or-function-text>",
                    },
                },
                {
                    "method": "code.write",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "extension": "<extension-name>",
                        "path": "<extension>.<form-or-object-name>.<routine-name>",
                        "routine_text": "<full-procedure-or-function-text>",
                        "mode": "apply",
                    },
                },
                {
                    "method": "bulk.execute",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "cache_profile": "normal",
                        "requests": [
                            {
                                "method": "code.search",
                                "payload": {
                                    "base_id": "<base_id-from-project-context>",
                                    "query": "<text-or-routine-fragment>",
                                    "source_state": "working",
                                },
                            },
                            {
                                "method": "metadata.resolve_overrides",
                                "payload": {
                                    "base_id": "<base_id-from-project-context>",
                                    "method_name": "<routine-name>",
                                    "source_state": "working",
                                },
                            },
                        ],
                    },
                },
                {
                    "method": "code.read",
                    "payload": {
                        "selector_token": "<selector-token-from-code-search>",
                        "include_line_numbers": True,
                        "max_chars": 20000,
                    },
                },
                {
                    "method": "templates.bindings",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "object_type": "<metadata-kind>",
                        "object_name": "<metadata-object-name>",
                    },
                },
                {
                    "method": "diagnostics.call_chain",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "entry_method": "<routine-name>",
                        "object_type": "<metadata-kind>",
                        "object_name": "<metadata-object-name>",
                    },
                },
                {
                    "method": "extensions.list",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "is_active": True,
                        "limit": 100,
                        "offset": 0,
                    },
                },
                {
                    "method": "modules.search",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "query": "<text-or-routine-fragment>",
                        "scan_limit": 500,
                        "resolve_owners": True,
                        "owner_scan_limit": 80,
                    },
                },
                {
                    "method": "modules.read",
                    "payload": {
                        "selector_token": "<selector-token-from-modules-search>",
                        "include_line_numbers": True,
                        "include_text": True,
                    },
                },
                {
                    "method": "metadata.write.preflight",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "extension": "<extension-name>",
                        "target": {"canonical_path": "ОбщаяФорма.<form-name>.<routine-name>", "kind": "module"},
                        "intent": {
                            "operation": "upsert_routine",
                            "routine_text": "<full-procedure-or-function-text>",
                        },
                    },
                },
                {
                    "method": "metadata.write",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "extension": "<extension-name>",
                        "target": {"canonical_path": "ОбщаяФорма.<form-name>.<routine-name>"},
                        "mode": "apply",
                        "routine_operation": "upsert",
                        "routine_text": "<full-procedure-or-function-text>",
                    },
                },
                {
                    "method": "metadata.write",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "target": {"kind": "schedule", "ref": "РегламентныеЗадания.<scheduled-job-name>"},
                        "schedule": {"begin_time": "09:00:00", "week_days": [1, 2, 3, 4, 5]},
                        "allow_saved_state_write": True,
                        "mode": "plan",
                    },
                },
                {
                    "method": "metadata.write",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "target": {
                            "area": "object",
                            "ref": "Справочник.<object-name>",
                            "property": "synonym",
                        },
                        "value": "<new-synonym>",
                        "mode": "plan",
                    },
                },
                {
                    "method": "metadata.write",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "target": {
                            "area": "object",
                            "operation": "add_attribute",
                            "template_member_ref": "Справочник.<object-name>.Реквизит.<template-attribute>",
                            "new_member_name": "<new-attribute-name>",
                        },
                        "mode": "plan",
                    },
                },
                {
                    "method": "metadata.form.command_button.write",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "extension": "<extension-name>",
                        "form": "<form-name>",
                        "command_name": "<command-name>",
                        "button_name": "<button-name>",
                        "title": "<command-title>",
                        "handler_name": "<routine-name>",
                        "handler_routine_text": "<full-procedure-text>",
                        "include_handler": True,
                        "mode": "apply",
                    },
                },
                {
                    "method": "metadata.form.command_button.verify",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "extension": "<extension-name>",
                        "form": "<form-name>",
                        "command_name": "<command-name>",
                        "button_name": "<button-name>",
                        "handler_name": "<routine-name>",
                    },
                },
                {
                    "method": "metadata.form.write_target.verify",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "extension": "<extension-name>",
                        "form": "<form-name>",
                        "command": "<command-or-element-name>",
                        "property": "title",
                    },
                },
                {
                    "method": "metadata.write.history",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "limit": 10,
                    },
                },
                {
                    "method": "metadata.write.rollback",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "operation_id": "<operation-id-from-metadata.write.history>",
                        "allow_sql_saved_state_rollback": True,
                    },
                },
                {
                    "method": "metadata.saved_state.prepare",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "layer": "extension_saved_state",
                        "ref": "<metadata-kind>.<metadata-object-name>",
                        "extension": "<extension-name>",
                        "mode": "plan",
                    },
                },
                {
                    "method": "metadata.saved_state.diff",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "ref": "<metadata-kind>.<metadata-object-name>",
                        "module_ordinal": 1,
                        "extension": "<extension-name-if-needed>",
                        "max_text_diff_lines": 80,
                    },
                },
                {
                    "method": "metadata.saved_state.status",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "layer": "extension_saved_state",
                        "limit": 200,
                    },
                },
                {
                    "method": "metadata.saved_state.changes.list",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "layer": "extension_saved_state",
                        "limit": 200,
                        "include_context": True,
                        "group_by_context": True,
                        "context_limit": 50,
                    },
                },
                {
                    "method": "configuration.activation.status",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "layer": "all",
                    },
                },
                {
                    "method": "configuration.activation.plan",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "layer": "extension_saved_state",
                    },
                },
                {
                    "method": "configuration.activation.request",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "layer": "extension_saved_state",
                    },
                },
                {
                    "method": "configuration.activation.execute",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "request_id": "<request-id-from-configuration.activation.request>",
                        "mode": "debug",
                        "confirm_activation": True,
                        "bridge_debug": True,
                    },
                },
                {
                    "method": "configuration.activation.request.cancel",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "request_id": "<activation-request-id>",
                        "confirm_cancel": True,
                    },
                },
                {
                    "method": "configuration.activation.capabilities",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "layer": "all",
                    },
                },
                {
                    "method": "configuration.activation.bridge.probe",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "layer": "extension_saved_state",
                        "timeout_seconds": 10,
                    },
                },
                {
                    "method": "configuration.activation.verify",
                    "payload": {
                        "base_id": "<base_id-from-project-context>",
                        "request_id": "<activation-request-id>",
                    },
                },
            ],
            "properties": {
                "method": {
                    "type": "string",
                "description": "Adapter method, for example metadata.objects.list, metadata.object.full, metadata.definition.find, metadata.route.resolve, extension.objects.find, templates.read, templates.analyze, templates.map, metadata.module_owner_cache.backfill, metadata.saved_state.ensure, metadata.saved_state.ensure.rollback, metadata.saved_state.prepare, metadata.saved_state.status, metadata.saved_state.diff, metadata.saved_state.changes.list, configuration.activation.status, configuration.activation.plan, configuration.activation.request, configuration.activation.request.status, configuration.activation.request.cancel, configuration.activation.audit, configuration.activation.capabilities, configuration.activation.bridge.probe, configuration.activation.execute, configuration.activation.verify, metadata.saved_state.forms.search, metadata.saved_state.modules.search, metadata.form.write_target.resolve, metadata.form.write_target.verify, metadata.module.write_apply, metadata.write.plan, metadata.write.preflight, metadata.write.capabilities, metadata.write, metadata.write.history, metadata.write.rollback, metadata.form.command_button.write, metadata.form.command_button.verify, code.write, metadata.form.element.write_apply, metadata.write_learning.capture_before, metadata.write_learning.capture_after, metadata.write_learning.diff, metadata.write_learning.infer_rule, modules.search, modules.read, code.search, code.read, code.symbol.resolve, templates.bindings, diagnostics.call_chain, bulk.execute, changes.propose, storage.saved_state.apply_proposal, storage.saved_state.rollback, or mcp.job.get. Live database methods require payload.base_id; placeholders in examples must be replaced from project/user context.",
                },
                "payload": {
                    "type": "object",
                    "description": (
                        "Method-specific JSON payload. For object-scoped methods, pass a selector as ref, "
                        "kind/name/guid, or MCP-friendly object_type/object_name/object_guid."
                    ),
                    "additionalProperties": True,
                },
            },
            "oneOf": [
                {
                    "properties": {
                        "method": {"const": "metadata.resolve_overrides"},
                        "payload": {
                            "type": "object",
                            "required": ["base_id", "method_name"],
                            "properties": {
                                "base_id": {"type": "string"},
                                **OBJECT_SELECTOR_SCHEMA_PROPERTIES,
                                "method_name": {"type": "string"},
                                "include_inactive": {"type": "boolean"},
                                "max_items": {"type": "integer", "minimum": 1, "maximum": 2000},
                            },
                        },
                    },
                    "required": ["method", "payload"],
                },
                {
                    "properties": {
                        "method": {"const": "metadata.objects.list"},
                        "payload": {
                            "type": "object",
                            "required": ["base_id"],
                            "properties": {
                                "base_id": {"type": "string"},
                                **OBJECT_SELECTOR_SCHEMA_PROPERTIES,
                                "name_pattern": {"type": "string"},
                                "limit": {"type": "integer", "minimum": 1},
                                "offset": {"type": "integer", "minimum": 0},
                            },
                        },
                    },
                    "required": ["method", "payload"],
                },
                {
                    "properties": {
                        "method": {"const": "metadata.object.full"},
                        "payload": {
                            "type": "object",
                            "required": ["base_id"],
                            "properties": {
                                "base_id": {"type": "string"},
                                **OBJECT_SELECTOR_SCHEMA_PROPERTIES,
                                "sections": {"type": "array", "items": {"type": "string"}},
                                "include_extensions": {"type": "boolean"},
                                "resolve_owner_chain": {"type": "boolean"},
                                "max_lines": {"type": "integer", "minimum": 1, "maximum": 10000},
                                "limit": {"type": "integer", "minimum": 1, "maximum": 500},
                                "offset": {"type": "integer", "minimum": 0},
                            },
                        },
                    },
                    "required": ["method", "payload"],
                },
                {
                    "properties": {
                        "method": {"const": "code.search"},
                        "payload": {
                            "type": "object",
                            "required": ["base_id", "query"],
                            "properties": {
                                "base_id": {"type": "string"},
                                "query": {"type": "string"},
                                **OBJECT_SELECTOR_SCHEMA_PROPERTIES,
                                "extension": {"type": "string"},
                                "routine_name": {"type": "string"},
                                "source_state": {"type": "string", "enum": ["working", "applied", "all"]},
                                "scope": {"type": "string", "enum": ["all", "object", "modules"]},
                                "regex": {"type": "boolean"},
                                "scan_limit": {"type": "integer", "minimum": 1, "maximum": 5000},
                                "limit": {"type": "integer", "minimum": 1, "maximum": 500},
                                "include_context": {"type": "boolean"},
                                "include_line_numbers": {"type": "boolean"},
                                "since_version": {"type": "string"},
                                "cache_profile": {"type": "string", "enum": ["short", "normal", "long"]},
                            },
                        },
                    },
                    "required": ["method", "payload"],
                },
                {
                    "properties": {
                        "method": {"const": "code.read"},
                        "payload": {
                            "type": "object",
                            "required": ["base_id"],
                            "properties": {
                                "base_id": {"type": "string"},
                                **OBJECT_SELECTOR_SCHEMA_PROPERTIES,
                                "id": {"type": "string"},
                                "routine_name": {"type": "string"},
                                "module_ref": {"type": "string"},
                                "source_state": {"type": "string", "enum": ["working", "applied", "all"]},
                                "include_line_numbers": {"type": "boolean"},
                                "include_text": {"type": "boolean"},
                                "max_chars": {"type": "integer", "minimum": 1, "maximum": 120000},
                                "include_overrides": {"type": "boolean"},
                                "cache_profile": {"type": "string", "enum": ["short", "normal", "long"]},
                            },
                        },
                    },
                    "required": ["method", "payload"],
                },
                {
                    "properties": {
                        "method": {"const": "code.write"},
                        "payload": {
                            "type": "object",
                            "required": ["base_id"],
                            "properties": {
                                "base_id": {"type": "string"},
                                **OBJECT_SELECTOR_SCHEMA_PROPERTIES,
                                "path": {"type": "string"},
                                "canonical_path": {"type": "string"},
                                "extension": {"type": "string"},
                                "module_ref": {"type": "string"},
                                "routine_name": {"type": "string"},
                                "routine_text": {"type": "string"},
                                "module_text": {"type": "string"},
                                "full_text": {"type": "string"},
                                "code": {"type": "string"},
                                "old": {"type": "string"},
                                "new": {"type": "string"},
                                "expected_sha1": {"type": "string"},
                                "expected_text_sha1": {"type": "string"},
                                "repository_lock": {"type": "object", "additionalProperties": True},
                                "write_context": {"type": "object", "additionalProperties": True},
                                "mode": {"type": "string", "enum": ["plan", "apply"]},
                                "include_storage": {"type": "boolean"},
                            },
                        },
                    },
                    "required": ["method", "payload"],
                },
                {
                    "properties": {
                        "method": {"const": "templates.bindings"},
                        "payload": {
                            "type": "object",
                            "required": ["base_id"],
                            "properties": {
                                "base_id": {"type": "string"},
                                **OBJECT_SELECTOR_SCHEMA_PROPERTIES,
                                "template_name": {"type": "string"},
                                "include_storage": {"type": "boolean"},
                                "cache_profile": {"type": "string", "enum": ["short", "normal", "long"]},
                            },
                        },
                    },
                    "required": ["method", "payload"],
                },
                {
                    "properties": {
                        "method": {"const": "diagnostics.call_chain"},
                        "payload": {
                            "type": "object",
                            "required": ["base_id", "entry_method"],
                        "properties": {
                            "base_id": {"type": "string"},
                            "entry_method": {"type": "string"},
                            "method_name": {"type": "string"},
                            **OBJECT_SELECTOR_SCHEMA_PROPERTIES,
                            "include_text": {"type": "boolean"},
                            "max_depth": {"type": "integer", "minimum": 1, "maximum": 50},
                            "max_nodes": {"type": "integer", "minimum": 1, "maximum": 2000},
                            "stop_on": {"type": "string"},
                            "resolve_owners": {"type": "boolean"},
                            "resolve_templates": {"type": "boolean"},
                            "cache_profile": {"type": "string", "enum": ["short", "normal", "long"]},
                        },
                    },
                },
                    "required": ["method", "payload"],
                },
                {
                    "properties": {
                        "method": {"const": "metadata.definition.find"},
                        "payload": {
                            "type": "object",
                            "required": ["base_id", "query"],
                            "properties": {
                                "base_id": {"type": "string"},
                                "query": {"type": "string"},
                                **OBJECT_SELECTOR_SCHEMA_PROPERTIES,
                                "extension": {"type": "string"},
                                "areas": {"type": "array", "items": {"type": "string"}},
                                "limit": {"type": "integer", "minimum": 1, "maximum": 10000},
                            },
                        },
                    },
                    "required": ["method", "payload"],
                },
                {
                    "properties": {
                        "method": {"const": "modules.search"},
                        "description": "Search BSL text in decoded modules with optional owner resolution.",
                        "payload": {
                            "type": "object",
                            "required": ["base_id", "query"],
                            "properties": {
                                "base_id": {"type": "string"},
                                "query": {"type": "string"},
                                **OBJECT_SELECTOR_SCHEMA_PROPERTIES,
                                "extension": {"type": "string"},
                                "scan_limit": {"type": "integer", "minimum": 1, "maximum": 5000},
                                "owner_scan_limit": {"type": "integer", "minimum": 1, "maximum": 200},
                                "limit": {"type": "integer", "minimum": 1, "maximum": 100},
                                "resolve_owners": {"type": "boolean"},
                                "include_storage": {"type": "boolean"},
                            },
                        },
                    },
                    "required": ["method", "payload"],
                },
                {
                    "properties": {
                        "method": {"const": "extensions.list"},
                        "description": "Enumerate extensions with active state and load order.",
                        "payload": {
                            "type": "object",
                            "required": ["base_id"],
                            "properties": {
                                "base_id": {"type": "string"},
                                "is_active": {"type": "boolean"},
                                "is_forbid_conflict": {"type": "boolean"},
                                "include_storage": {"type": "boolean"},
                                "limit": {"type": "integer", "minimum": 1},
                                "offset": {"type": "integer", "minimum": 0},
                            },
                        },
                    },
                    "required": ["method", "payload"],
                },
                {
                    "properties": {
                        "method": {"const": "bulk.execute"},
                        "payload": {
                            "type": "object",
                            "required": ["base_id", "requests"],
                            "properties": {
                                "base_id": {"type": "string"},
                                "cache_profile": {"type": "string", "enum": ["short", "normal", "long"]},
                                "requests": {
                                    "type": "array",
                                    "maxItems": 30,
                                    "items": {
                                        "type": "object",
                                        "required": ["method", "payload"],
                                        "properties": {
                                            "method": {"type": "string"},
                                            "payload": {"type": "object"},
                                        },
                                    },
                                },
                                "limit": {"type": "integer", "minimum": 1, "maximum": 50},
                                "offset": {"type": "integer", "minimum": 0},
                            },
                        },
                    },
                    "required": ["method", "payload"],
                },
                {
                    "properties": {
                        "method": {"const": "modules.read"},
                        "description": "Read module source by module_ref or owner selector.",
                        "payload": {
                            "type": "object",
                            "required": ["base_id"],
                            "properties": {
                                "base_id": {"type": "string"},
                                "module_ref": {"type": "string"},
                                **OBJECT_SELECTOR_SCHEMA_PROPERTIES,
                                "module_type": {"type": "string"},
                                "include_line_numbers": {"type": "boolean"},
                                "include_text": {"type": "boolean"},
                                "include_storage": {"type": "boolean"},
                                "preview": {"type": "boolean"},
                                "max_chars": {"type": "integer", "minimum": 1, "maximum": 100000},
                            },
                        },
                    },
                    "required": ["method", "payload"],
                },
            ],
            "additionalProperties": False,
        },
    },
    {
        "name": "onec_job_get",
        "description": "Return the status/result of a long 1C adapter job started by onec_request.",
        "inputSchema": {
            "type": "object",
            "required": ["job_id"],
            "properties": {
                "job_id": {
                    "type": "string",
                    "description": "Job id returned by onec_request.",
                },
                "consume": {
                    "type": "boolean",
                    "description": "Remove a completed job after reading it.",
                },
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "onec_job_cancel",
        "description": "Request cancellation of a long 1C adapter job started by onec_request.",
        "inputSchema": {
            "type": "object",
            "required": ["job_id"],
            "properties": {
                "job_id": {
                    "type": "string",
                    "description": "Job id returned by onec_request.",
                },
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "access_role_users",
        "description": "Find users that receive a 1C/BSP role through access profiles and groups. Supports fuzzy role phrases such as 'запись изменение номенклатура поставщиков'.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id", "role"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "role": {"type": "string", "description": "Role id, exact role name, substring, or fuzzy natural-language role phrase."},
                "limit": {"type": "integer", "minimum": 1, "maximum": 20000, "default": 20000},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 120},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "access_role_profiles",
        "description": "Find 1C/BSP access profiles that include a role, plus access groups using those profiles.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id", "role"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "role": {"type": "string", "description": "Role id, exact role name, substring, or fuzzy natural-language role phrase."},
                "limit": {"type": "integer", "minimum": 1, "maximum": 20000, "default": 1000},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 120},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "access_role_audit_export",
        "description": "Export flat 1C/BSP role audit rows: role -> profile -> access group -> user. Can include CSV text.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id", "role"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "role": {"type": "string", "description": "Role id, exact role name, substring, or fuzzy natural-language role phrase."},
                "format": {"type": "string", "enum": ["json", "csv"], "default": "json"},
                "limit": {"type": "integer", "minimum": 1, "maximum": 20000, "default": 20000},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 120},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "access_role_audit_analyze",
        "description": "Analyze a 1C/BSP role audit chain and return risk findings for broad groups, many users, external users, fuzzy matches, and multiple paths.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id", "role"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "role": {"type": "string", "description": "Role id, exact role name, substring, or fuzzy natural-language role phrase."},
                "user_threshold": {"type": "integer", "minimum": 1, "maximum": 100000, "default": 50},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 120},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "access_role_audit_compare_latest",
        "description": "Compare the latest two local access audit reports for a base/role and return added, removed, and changed access-path users.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id", "role"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "role": {"type": "string", "description": "Role query used in saved access audit reports."},
                "report_root": {"type": "string", "description": "Optional local report root. Defaults to reports/1c-access."},
                "write_artifacts": {"type": "boolean", "default": True},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "infobase_users_search",
        "description": "Default user search for 1C infobase users visible in Configurator. Authoritative for platform identity, authentication flags, platform administrator, and RolesID. Do not substitute BSP catalog users or profiles for Configurator role assignments.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "query": {"type": "string", "description": "Optional Configurator user name, full name, or platform user id. Empty returns the first page."},
                "limit": {"type": "integer", "minimum": 1, "maximum": 500, "default": 20},
                "scan_limit": {"type": "integer", "minimum": 1, "maximum": 50000, "default": 5000},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 30},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "infobase_user_get",
        "description": "Get one 1C infobase/Configurator user by exact name or platform id. Exact configuration role names are reported as runtime-required when SQL exposes only RolesID.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id", "user"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "user": {"type": "string", "description": "Exact Configurator user name or platform user id."},
                "scan_limit": {"type": "integer", "minimum": 1, "maximum": 50000, "default": 5000},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 30},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "infobase_user_password_capabilities",
        "description": "Report whether protected password set/clear operations are ready for Configurator users in a concrete infobase. No password is accepted by this diagnostic tool.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."}
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "infobase_user_password_status",
        "description": "Read whether one exact infobase/Configurator user has an empty or non-empty password without exposing hashes or protected Data. Also reports whether standard authentication is enabled.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id", "user"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "user": {"type": "string", "description": "Exact Configurator user name or platform user id."},
                "scan_limit": {"type": "integer", "minimum": 1, "maximum": 50000, "default": 5000},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 30}
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "infobase_user_password_set",
        "description": "Set a new password for one exact infobase/Configurator user through a guarded SQL transaction on dbo.v8users.Data. The adapter writes the normal and uppercase SHA-1/Base64 pair, verifies readback, and never echoes or persists the clear-text password.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id", "user", "confirm_user_id", "new_password", "allow_password_change"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "user": {"type": "string", "description": "Exact Configurator user name or platform user id."},
                "confirm_user_id": {"type": "string", "description": "Exact 32-hex id returned by infobase_user_get."},
                "new_password": {"type": "string", "minLength": 1, "maxLength": 1024, "writeOnly": True, "description": "Secret new password. It is sent only to the configured 1C runtime bridge and is never returned."},
                "allow_password_change": {"type": "boolean", "description": "Must be true after reviewing the exact target."},
                "allow_administrator_password_change": {"type": "boolean", "default": False, "description": "Additional confirmation required when the selected user is a platform administrator."},
                "request_id": {"type": "string", "description": "Optional idempotency/audit request id."},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 30}
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "infobase_user_password_clear",
        "description": "Remove (clear) the password of one exact infobase/Configurator user through a guarded SQL transaction on dbo.v8users.Data. The adapter changes only the current password hash pair and verifies readback. No password argument is accepted.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id", "user", "confirm_user_id", "allow_password_clear"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "user": {"type": "string", "description": "Exact Configurator user name or platform user id."},
                "confirm_user_id": {"type": "string", "description": "Exact 32-hex id returned by infobase_user_get."},
                "allow_password_clear": {"type": "boolean", "description": "Must be true after reviewing the exact target."},
                "allow_administrator_password_change": {"type": "boolean", "default": False, "description": "Additional confirmation required when the selected user is a platform administrator."},
                "request_id": {"type": "string", "description": "Optional idempotency/audit request id."},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 30}
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "access_user_explain",
        "description": "Explain BSP catalog access for one explicitly BSP-scoped user: groups, profiles, technical roles, permissions, and access keys. This does not prove Configurator authentication or direct platform role assignments; ordinary 'user' requests must start with infobase_users_search.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id", "user"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "user": {"type": "string", "description": "User name, user id/ref, or ref tail."},
                "preset": {"type": "string", "default": "bsp"},
                "object": {"type": "string"},
                "action": {"type": "string"},
                "resolve_records": {"type": "boolean"},
                "max_effective_permissions_per_user": {"type": "integer", "minimum": 1, "maximum": 20000},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 120},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "access_users_search",
        "description": "Search BSP catalog users by name, login, id/ref tail, or fuzzy fragment. Use only for explicit BSP group/profile/RLS questions; ordinary 'users' means infobase/Configurator users and must use infobase_users_search.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id", "query"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "query": {"type": "string", "description": "User name, login fragment, id/ref tail, or fuzzy text."},
                "limit": {"type": "integer", "minimum": 1, "maximum": 200, "default": 20},
                "scan_limit": {"type": "integer", "minimum": 1, "maximum": 50000, "default": 20000},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 120},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "access_object_explain",
        "description": "Explain who can see a BSP-protected data record. Prefer public object_ref plus record_ref; legacy raw BSP fields remain available.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "object_ref": {"type": "string", "description": "Public metadata ref, for example Справочники.Номенклатура."},
                "record_ref": {"type": "string", "description": "Concrete 1C application-data record reference."},
                "object": {"type": "string", "description": "Legacy raw BSP object value."},
                "object_id": {"type": "string", "description": "Legacy raw BSP object_id; prefer record_ref."},
                "object_sql_number": {"type": "integer", "description": "Legacy physical selector; prefer object_ref."},
                "access_key": {"type": "string"},
                "limit": {"type": "integer", "minimum": 1, "maximum": 20000, "default": 1000},
                "subject_limit": {"type": "integer", "minimum": 1, "maximum": 20000, "default": 1000},
                "max_resolved_records": {"type": "integer", "minimum": 0, "maximum": 5000, "default": 200},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 120},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "access_keys_query",
        "description": "Page through BSP access key registers. kind selects the query area, not a metadata kind; in object mode prefer object_ref plus optional record_ref.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "kind": {"type": "string", "description": "Query area: group, user_set, object, set, or all. This is not a 1C metadata kind."},
                "area": {"type": "string", "description": "Alias of kind."},
                "group": {"type": "string"},
                "user": {"type": "string"},
                "user_set": {"type": "string"},
                "object_ref": {"type": "string", "description": "Public metadata ref, for example Справочники.Номенклатура."},
                "record_ref": {"type": "string", "description": "Concrete 1C application-data record reference."},
                "object": {"type": "string", "description": "Legacy raw BSP object value."},
                "object_id": {"type": "string", "description": "Legacy raw BSP object_id; prefer record_ref."},
                "object_sql_number": {"type": "integer", "description": "Legacy physical selector; prefer object_ref."},
                "access_key": {"type": "string"},
                "resolve_records": {"type": "boolean", "default": False},
                "max_resolved_records": {"type": "integer", "minimum": 0, "maximum": 5000, "default": 200},
                "limit": {"type": "integer", "minimum": 1, "maximum": 20000, "default": 1000},
                "offset": {"type": "integer", "minimum": 0, "default": 0},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 120},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "access_object_keys_resolve",
        "description": "Return BSP object access-key rows and resolve record presentations. Prefer public object_ref plus optional record_ref.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "object_ref": {"type": "string", "description": "Public metadata ref, for example Справочники.Номенклатура."},
                "record_ref": {"type": "string", "description": "Concrete 1C application-data record reference."},
                "object": {"type": "string", "description": "Legacy raw BSP object value."},
                "object_id": {"type": "string", "description": "Legacy raw BSP object_id; prefer record_ref."},
                "object_sql_number": {"type": "integer", "description": "Legacy physical selector; prefer object_ref."},
                "access_key": {"type": "string"},
                "limit": {"type": "integer", "minimum": 1, "maximum": 20000, "default": 1000},
                "offset": {"type": "integer", "minimum": 0, "default": 0},
                "max_resolved_records": {"type": "integer", "minimum": 0, "maximum": 5000, "default": 200},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 120},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "access_object_roles",
        "description": "Find BSP roles that grant permissions for one metadata object and summarize read/insert/update/delete rights. Accepts object selectors by ref, kind/name, or GUID aliases.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "ref": {"type": "string", "description": "Public object ref such as InformationRegister.УОП_АктуальныеСпецификации or РегистрСведений.УОП_АктуальныеСпецификации."},
                "kind": {"type": "string"},
                "name": {"type": "string"},
                "guid": {"type": "string"},
                "object_type": {"type": "string"},
                "object_name": {"type": "string"},
                "object_guid": {"type": "string"},
                "action": {"type": "string", "description": "Optional right filter such as read, insert, update, delete, Просмотр, Добавление, Изменение, or Удаление."},
                "limit": {"type": "integer", "minimum": 1, "maximum": 20000, "default": 200},
                "max_effective_permissions_per_user": {"type": "integer", "minimum": 0, "maximum": 200000, "default": 0},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 120},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "access_object_subjects",
        "description": "Find roles, profiles, access groups, and users that receive permissions for one metadata object. Accepts names/public refs and resolves internal identifiers automatically.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "ref": {"type": "string", "description": "Public object ref such as РегистрСведений.УОП_АктуальныеСпецификации or InformationRegister.Name."},
                "kind": {"type": "string"},
                "name": {"type": "string"},
                "guid": {"type": "string"},
                "object_type": {"type": "string"},
                "object_name": {"type": "string"},
                "object_guid": {"type": "string"},
                "action": {"type": "string", "description": "Optional right filter such as read, write, insert, update, delete, Просмотр, Добавление, or Изменение."},
                "limit": {"type": "integer", "minimum": 1, "maximum": 20000, "default": 1000},
                "max_effective_permissions_per_user": {"type": "integer", "minimum": 0, "maximum": 200000, "default": 0},
                "include_access_key_scope": {"type": "boolean", "default": False, "description": "Also return BSP subject access key scope. This is useful for data restriction diagnostics and can be slower."},
                "access_key_scope_limit": {"type": "integer", "minimum": 1, "maximum": 200000, "default": 20000, "description": "Maximum rows to read from each BSP access-key extractor when include_access_key_scope is true."},
                "access_key_scope_subject_limit": {"type": "integer", "minimum": 1, "maximum": 200000, "default": 20000, "description": "Maximum matched groups/users to include in access-key scope diagnostics. This is independent from the response limit."},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 120},
            },
            "additionalProperties": False,
        },
    },
    {
        "name": "access_rls_discover",
        "description": "Discover metadata candidates for BSP/RLS/data restriction extraction by names such as Огранич, Доступ, RLS, and Ключ. Returns storage routes and fields for building a verified extractor.",
        "inputSchema": {
            "type": "object",
            "required": ["base_id"],
            "properties": {
                "base_id": {"type": "string", "description": "Concrete 1C base id."},
                "terms": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Optional search terms. Defaults to Огранич, Доступ, RLS, Ключ.",
                },
                "limit": {"type": "integer", "minimum": 1, "maximum": 500, "default": 50},
                "timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 120, "default": 120},
            },
            "additionalProperties": False,
        },
    },
]


def keep_onec_request_schema_generic() -> None:
    for tool in TOOLS:
        if tool.get("name") != "onec_request":
            continue
        schema = tool.get("inputSchema")
        if not isinstance(schema, dict):
            return
        # onec_request is the adapter pass-through. Do not enumerate adapter
        # methods here; the REST adapter owns method-specific contracts.
        schema.pop("oneOf", None)
        schema["additionalProperties"] = False
        return


keep_onec_request_schema_generic()


class AdapterError(RuntimeError):
    def __init__(self, message: str, *, status: int | None = None, body: str = "") -> None:
        super().__init__(message)
        self.status = status
        self.body = body


def adapter_url() -> str:
    return os.environ.get("ONEC_ADAPTER_URL", DEFAULT_ADAPTER_URL).rstrip("/")


def adapter_token() -> str:
    return os.environ.get("ONEC_ADAPTER_TOKEN", "")


def adapter_timeout() -> float:
    try:
        return max(0.5, float(os.environ.get("ONEC_ADAPTER_TIMEOUT_SECONDS", "120")))
    except ValueError:
        return 120.0


def job_fast_wait_seconds() -> float:
    try:
        return max(0.0, min(float(os.environ.get("ONEC_MCP_JOB_FAST_WAIT_SECONDS", "1.5")), 3.0))
    except ValueError:
        return 1.5


def job_heartbeat_seconds() -> float:
    try:
        return max(0.2, float(os.environ.get("ONEC_MCP_JOB_HEARTBEAT_SECONDS", "1.0")))
    except ValueError:
        return 1.0


def job_timeout_seconds(payload: dict[str, Any]) -> float:
    raw_timeout = payload.get("timeout_seconds")
    if raw_timeout is None:
        raw_timeout = os.environ.get("ONEC_MCP_JOB_TIMEOUT_SECONDS", "600")
    try:
        return max(1.0, float(raw_timeout))
    except (TypeError, ValueError):
        return 600.0


JOB_TTL_SECONDS = 3600.0


def truthy(value: Any) -> bool:
    if isinstance(value, bool):
        return value
    if value is None:
        return False
    return str(value).strip().lower() in {"1", "true", "yes", "y", "on"}


def normalize_source_mode(value: Any) -> str:
    value_text = str(value or "").strip().lower()
    if value_text not in SOURCE_MODES:
        return DEFAULT_SOURCE_MODE
    return value_text


def normalize_source_state(value: Any, source_mode: str) -> str:
    value_text = str(value or "").strip().lower()
    if value_text == "runtime":
        return "applied"
    if value_text == "designer":
        return "working"
    if value_text in SOURCE_STATES:
        return value_text
    # Agents program against the logical Designer configuration by default.
    # Runtime remains explicit through source_mode=runtime or source_state=applied.
    return "applied" if source_mode == "runtime" else "working"


def normalize_configuration_view(value: Any, source_state: str) -> tuple[str, str]:
    view = str(value or "").strip().casefold()
    if not view:
        return ("effective_working" if source_state == "working" else "runtime_applied" if source_state == "applied" else "compare", source_state)
    mapped = CONFIGURATION_VIEW_TO_SOURCE_STATE.get(view)
    if not mapped:
        return "", ""
    if source_state and source_state != mapped:
        return "", ""
    canonical = "effective_working" if mapped == "working" else "runtime_applied" if mapped == "applied" else "compare"
    return canonical, mapped


def normalize_cache_policy(value: Any, source_mode: str) -> str:
    value_text = str(value or "").strip().lower()
    if value_text not in CACHE_POLICIES:
        return DEFAULT_CACHE_POLICY_BY_SOURCE_MODE.get(source_mode, "ttl")
    if source_mode == "designer" and value_text == "ttl":
        return "none"
    return value_text


def is_guid_text(value: Any) -> bool:
    if not isinstance(value, str):
        return False
    return GUID_RE.match(value.strip().lower()) is not None


def metadata_cache_lookup_identity(base_id: str, guid: str) -> dict[str, Any] | None:
    if not base_id or not is_guid_text(guid):
        return None
    try:
        result = call_adapter_method("metadata.cache.lookup", {"base_id": base_id, "guid": guid})
    except AdapterError:
        return None
    if not isinstance(result, dict) or result.get("status") != "ok":
        return None
    identity = result.get("object")
    if not isinstance(identity, dict):
        return None
    return identity


def metadata_cache_lookup_identity_cached(
    base_id: str,
    guid: str,
    owner_cache: dict[str, dict[str, Any] | None],
    lookup_budget: dict[str, int],
) -> dict[str, Any] | None:
    normalized_guid = str(guid).strip().lower()
    if not is_guid_text(normalized_guid):
        return None
    if normalized_guid in owner_cache:
        return owner_cache.get(normalized_guid)
    if lookup_budget.get("remaining", 0) <= 0:
        return None
    lookup_budget["remaining"] = max(0, int(lookup_budget.get("remaining", 0)) - 1)
    identity = metadata_cache_lookup_identity(base_id, normalized_guid)
    owner_cache[normalized_guid] = identity
    return identity


def resolve_owner_metadata(base_id: str, owner_payload: Any, context_payload: Any, owner_cache: dict[str, dict[str, Any] | None], lookup_budget: dict[str, int]) -> dict[str, Any] | None:
    if not isinstance(owner_payload, dict):
        return None
    if owner_payload.get("status") == "resolved" and owner_payload.get("name") and owner_payload.get("kind") and owner_payload.get("guid"):
        return owner_payload
    owner_guid = owner_payload.get("guid") or owner_payload.get("id")
    if not is_guid_text(owner_guid):
        if isinstance(context_payload, dict):
            owner_guid = context_payload.get("guid") or context_payload.get("owner_guid")
        if not is_guid_text(owner_guid):
            return owner_payload
    identity = metadata_cache_lookup_identity_cached(base_id, str(owner_guid), owner_cache, lookup_budget)
    if not identity:
        return owner_payload
    return {
        "status": "resolved",
        "kind": identity.get("kind"),
        "name": identity.get("name"),
        "synonym": identity.get("synonym"),
        "guid": str(owner_guid).lower(),
    }


def enrich_owner_metadata_in_data(payload: dict[str, Any], data: Any) -> Any:
    base_id = str(payload.get("base_id") or "").strip()
    if not is_guid_text(base_id):
        return data
    owner_cache: dict[str, dict[str, Any] | None] = {}
    lookup_budget = {"remaining": OWNER_IDENTITY_LOOKUP_LIMIT}
    requested_object_type = str(payload.get("object_type") or "")
    requested_object_name = str(payload.get("object_name") or "")
    requested_object_guid = str(payload.get("object_guid") or "")

    def walk(obj: Any, context: dict[str, Any] | None = None) -> Any:
        if not isinstance(obj, dict):
            return [walk(item, context) for item in obj] if isinstance(obj, list) else obj
        enriched = dict(obj)
        current_context = context if isinstance(context, dict) else None
        if current_context is None:
            current_context = enriched.get("read_selector") if isinstance(enriched.get("read_selector"), dict) else None
        owner_fields = [
            "owner",
            "resolved_owner",
        ]
        for key in owner_fields:
            if key not in enriched:
                continue
            context_payload = current_context if isinstance(current_context, dict) else {}
            owner_payload = enriched.get(key)
            resolved_owner = resolve_owner_metadata(base_id, owner_payload, context_payload, owner_cache, lookup_budget)
            normalized_owner = _normalize_owner_output(
                base_id,
                resolved_owner,
                fallback_kind=str(context_payload.get("kind") or context_payload.get("object_type") or requested_object_type),
                fallback_name=str(context_payload.get("name") or context_payload.get("object_name") or requested_object_name),
                fallback_guid=str(context_payload.get("guid") or context_payload.get("object_guid") or requested_object_guid),
            )
            if normalized_owner is None and owner_payload is not None:
                normalized_owner = owner_payload if isinstance(owner_payload, dict) else {"status": "unknown"}
            enriched[key] = normalized_owner
            if isinstance(normalized_owner, dict):
                enriched[f"{key}_display"] = {
                    "kind": normalized_owner.get("kind"),
                    "name": normalized_owner.get("name"),
                    "guid": normalized_owner.get("guid"),
                }

        for key, value in list(enriched.items()):
            if key in owner_fields:
                continue
            nested = walk(value, current_context)
            if isinstance(nested, dict):
                current = dict(nested)
            else:
                current = nested
            enriched[key] = current
        return enriched

    return walk(data)


def _build_object_display_payload(raw: dict[str, Any] | Any) -> dict[str, Any] | None:
    if not isinstance(raw, dict):
        return None
    kind = raw.get("kind") or raw.get("object_type") or raw.get("type")
    name = raw.get("name") or raw.get("synonym") or raw.get("title")
    guid = raw.get("guid") or raw.get("id")
    if not (kind or name or guid):
        return None
    return {"kind": kind, "name": name, "guid": guid}


def _ensure_display_fields_for_object_item(item: dict[str, Any], *, fallback_kind: str | None = None) -> None:
    if not isinstance(item, dict):
        return
    if item.get("object_display") is None:
        built = _build_object_display_payload(item)
        if built is not None:
            item["object_display"] = built
        else:
            fallback_name = item.get("name") or item.get("synonym") or item.get("title") or item.get("caption")
            if fallback_name or item.get("guid") or item.get("id"):
                item["object_display"] = {
                    "kind": item.get("kind") or fallback_kind or item.get("object_type") or item.get("type"),
                    "name": fallback_name,
                    "guid": item.get("guid") or item.get("id") or item.get("ref"),
                }
    owner_payload = item.get("owner") if isinstance(item.get("owner"), dict) else None
    if owner_payload is not None and item.get("owner_display") is None:
        owner_display = _build_object_display_payload(owner_payload)
        if owner_display is not None:
            item["owner_display"] = owner_display
        elif owner_payload.get("kind") or owner_payload.get("name") or owner_payload.get("guid"):
            item["owner_display"] = {
                "kind": owner_payload.get("kind"),
                "name": owner_payload.get("name"),
                "guid": owner_payload.get("guid"),
            }


def _enrich_nested_object_display(section: dict[str, Any], fallback_kind: str | None = None) -> None:
    if not isinstance(section, dict):
        return
    for key in ("items", "attributes", "tabular_sections", "properties", "fields", "nodes"):
        nested_items = section.get(key)
        if not isinstance(nested_items, list):
            continue
        for nested in nested_items:
            if not isinstance(nested, dict):
                continue
            _ensure_display_fields_for_object_item(nested, fallback_kind=fallback_kind)


def _enrich_metadata_objects_list_display(payload: dict[str, Any], result: Any) -> Any:
    _ = payload  # kept for future heuristics and compatibility
    if not isinstance(result, dict) and not isinstance(result, list):
        return result
    enriched = result
    candidates: list[Any] = []
    if isinstance(result, list):
        candidates = result
    elif isinstance(result, dict):
        for container_key in ("items", "objects", "result", "data"):
            container = result.get(container_key)
            if isinstance(container, list):
                candidates = container
                break
    for item in candidates:
        if not isinstance(item, dict):
            continue
        if "object_display" in item:
            continue
        item_display = _build_object_display_payload(item)
        if item_display is not None:
            item["object_display"] = item_display
    return enriched


def _enrich_metadata_object_full_display(payload: dict[str, Any], result: Any) -> Any:
    if not isinstance(result, dict):
        return result
    enriched = result
    object_kind = None
    if isinstance(enriched.get("query"), dict):
        object_kind = enriched["query"].get("kind") or enriched["query"].get("object_type")

    query = enriched.get("query")
    if isinstance(query, dict) and not enriched.get("object_display"):
        owner_display = {
            "kind": query.get("kind"),
            "name": query.get("name"),
            "guid": query.get("guid"),
        }
        if any(owner_display.get(key) for key in ("kind", "name", "guid")):
            enriched["object_display"] = owner_display
    if "object" in enriched and isinstance(enriched.get("object"), dict) and "object_display" not in enriched["object"]:
        object_data = _build_object_display_payload(enriched.get("object") or {})
        if object_data is not None:
            enriched["object"]["object_display"] = object_data

    for section_key in ("forms", "templates", "commands", "modules"):
        section_items = enriched.get(section_key)
        if not isinstance(section_items, list):
            continue
        for item in section_items:
            if not isinstance(item, dict):
                continue
            if "object_display" not in item:
                candidate = _build_object_display_payload(item)
                if candidate is not None:
                    item["object_display"] = candidate
                elif object_kind:
                    candidate_name = item.get("name") or item.get("synonym")
                    candidate_guid = item.get("guid") or item.get("id")
                    if candidate_name or candidate_guid:
                        item["object_display"] = {"kind": object_kind, "name": candidate_name, "guid": candidate_guid}
    semantic = enriched.get("semantic")
    if isinstance(semantic, dict):
        sections = semantic.get("sections")
        if isinstance(sections, list):
            for section in sections:
                if not isinstance(section, dict):
                    continue
                if section.get("name") and not section.get("object_display"):
                    section["object_display"] = {
                        "kind": section.get("kind") or object_kind,
                        "name": section.get("name"),
                        "guid": section.get("guid") or section.get("id"),
                    }
                if section.get("owner") and isinstance(section.get("owner"), dict) and section.get("owner_display") is None:
                    owner_payload = section.get("owner")
                    owner_display = _build_object_display_payload(owner_payload)
                    if owner_display is not None:
                        section["owner_display"] = owner_display
                    else:
                        section["owner_display"] = {
                            "kind": owner_payload.get("kind"),
                            "name": owner_payload.get("name"),
                            "guid": owner_payload.get("guid"),
                        }
                _enrich_nested_object_display(section, fallback_kind=str(object_kind or section.get("kind") or ""))
    return enriched


def _enrich_modules_search_display(payload: dict[str, Any], result: Any) -> Any:
    _ = payload  # preserved for compatibility
    if not isinstance(result, dict):
        return result
    candidates: list[Any] = []
    container_keys = ("items", "result", "modules", "matches", "data")
    for key in container_keys:
        container = result.get(key)
        if isinstance(container, list):
            candidates = container
            break
    if not candidates and isinstance(result.get("result"), dict):
        nested = result.get("result")
        for key in container_keys:
            nested_container = nested.get(key)
            if isinstance(nested_container, list):
                candidates = nested_container
                break
    for item in candidates:
        if not isinstance(item, dict):
            continue
        read_selector = item.get("read_selector") if isinstance(item.get("read_selector"), dict) else {}
        owner_payload = item.get("owner")
        if isinstance(owner_payload, dict):
            owner_display = _build_object_display_payload(owner_payload)
            if owner_display is not None:
                item["owner_display"] = owner_display
            elif (
                owner_payload.get("kind")
                or owner_payload.get("name")
                or owner_payload.get("guid")
                or owner_payload.get("id")
            ):
                item["owner_display"] = {
                    "kind": owner_payload.get("kind"),
                    "name": owner_payload.get("name"),
                    "guid": owner_payload.get("guid") or owner_payload.get("id"),
                }
        module_ref = item.get("module_ref") or read_selector.get("module_ref")
        if "source_ref_display" not in item and (module_ref or item.get("module_name")):
            selector_display = _build_object_display_payload(read_selector)
            item["source_ref_display"] = {
                "kind": selector_display.get("kind") if selector_display else "module",
                "name": selector_display.get("name") if selector_display else item.get("module_name") or item.get("name"),
                "guid": module_ref,
                "method": read_selector.get("method"),
            }
        if "object_display" not in item:
            object_display = _build_object_display_payload(item)
            if object_display is None:
                object_display = {
                    "kind": item.get("object_type") or item.get("kind") or item.get("owner_kind"),
                    "name": item.get("name") or item.get("module_name") or item.get("routine_name"),
                    "guid": item.get("guid") or item.get("id") or module_ref,
                }
            item["object_display"] = object_display
    return result


def maybe_enrich_owner_fields(method: str, payload: dict[str, Any] | None, result: Any) -> Any:
    if method not in {
        "modules.search",
        "modules.read",
        "code.search",
        "code.read",
        "metadata.object.full",
        "metadata.objects.list",
        "metadata.resolve_overrides",
        "templates.bindings",
        "templates.read",
        "templates.analyze",
        "templates.map",
        "extension.objects.find",
        "metadata.route.resolve",
        "diagnostics.call_chain",
    }:
        return result
    if not isinstance(payload, dict):
        return result
    result = enrich_owner_metadata_in_data(payload, result)
    if method == "metadata.object.full":
        result = _enrich_metadata_object_full_display(payload, result)
    elif method == "metadata.objects.list":
        result = _enrich_metadata_objects_list_display(payload, result)
    elif method in {"modules.search", "code.search"}:
        result = _enrich_modules_search_display(payload, result)
    elif method in {"templates.bindings", "templates.read", "templates.analyze", "templates.map", "extension.objects.find", "metadata.route.resolve", "diagnostics.call_chain", "code.read", "modules.read"}:
        if method == "modules.read" and isinstance(result, dict):
            owner_payload = result.get("owner")
            if isinstance(owner_payload, dict):
                owner_display = _build_object_display_payload(owner_payload)
                if owner_display is not None:
                    result["owner_display"] = owner_display
                elif owner_payload.get("kind") or owner_payload.get("name") or owner_payload.get("guid"):
                    result["owner_display"] = {
                        "kind": owner_payload.get("kind"),
                        "name": owner_payload.get("name"),
                        "guid": owner_payload.get("guid"),
                    }
            module_payload = result.get("module") if isinstance(result.get("module"), dict) else None
            if isinstance(module_payload, dict):
                module_display = _build_object_display_payload(module_payload)
                if module_display is not None:
                    result["module_display"] = module_display
                elif module_payload.get("name") or module_payload.get("module_ref") or module_payload.get("guid"):
                    result["module_display"] = {
                        "kind": module_payload.get("kind") or module_payload.get("module_type") or "module",
                        "name": module_payload.get("name"),
                        "guid": module_payload.get("module_ref") or module_payload.get("guid") or module_payload.get("id"),
                    }
        if isinstance(result, dict) and isinstance(result.get("items"), list):
            for item in result.get("items") or []:
                if not isinstance(item, dict):
                    continue
                owner_payload = item.get("owner")
                if isinstance(owner_payload, dict):
                    owner_display = _build_object_display_payload(owner_payload)
                    if owner_display is not None:
                        item["owner_display"] = owner_display
                    elif owner_payload.get("kind") or owner_payload.get("name") or owner_payload.get("guid"):
                        item["owner_display"] = {
                            "kind": owner_payload.get("kind"),
                            "name": owner_payload.get("name"),
                            "guid": owner_payload.get("guid"),
                        }
                module_payload = item.get("module") if isinstance(item.get("module"), dict) else None
                if isinstance(module_payload, dict):
                    module_display = _build_object_display_payload(module_payload)
                    if module_display is not None:
                        item["module_display"] = module_display
                    elif module_payload.get("name") or module_payload.get("guid"):
                        item["module_display"] = {
                            "kind": module_payload.get("kind") or module_payload.get("module_type") or "module",
                            "name": module_payload.get("name"),
                            "guid": module_payload.get("module_ref") or module_payload.get("guid") or module_payload.get("id"),
                        }
        if method == "diagnostics.call_chain":
            if isinstance(result, dict):
                diagnostics = result.get("diagnostics")
                if isinstance(diagnostics, dict):
                    edges = diagnostics.get("edges")
                    if isinstance(edges, list):
                        for edge in edges:
                            if not isinstance(edge, dict):
                                continue
                            edge_owner = edge.get("owner") if isinstance(edge.get("owner"), dict) else None
                            if isinstance(edge_owner, dict):
                                edge["owner_display"] = _build_object_display_payload(edge_owner) or edge_owner
    return result


def coerce_int(value: Any, default: int | None = None, *, minimum: int | None = None, maximum: int | None = None) -> int | None:
    try:
        parsed = int(value)
    except (TypeError, ValueError):
        return default
    if minimum is not None:
        parsed = max(minimum, parsed)
    if maximum is not None:
        parsed = min(maximum, parsed)
    return parsed


def normalize_cache_profile(payload: dict[str, Any]) -> str:
    profile = str(payload.get("cache_profile") or "normal").strip().lower()
    if profile not in NEW_METHOD_CACHE_TTL_SECONDS:
        return "normal"
    return profile


def cache_ttl_for_profile(profile: str) -> int:
    return int(NEW_METHOD_CACHE_TTL_SECONDS.get(profile, NEW_METHOD_CACHE_TTL_SECONDS["normal"]))


def make_cache_key(method: str, payload: dict[str, Any], version: int = NEW_API_VERSION) -> str:
    payload_snapshot = dict(payload)
    payload_snapshot.pop("_mcp_request_id", None)
    payload_snapshot.pop("consume", None)
    payload_snapshot.pop("mcp_async", None)
    payload_snapshot.pop("_mcp_async", None)
    payload_snapshot.pop("mcp_sync", None)
    payload_snapshot.pop("_mcp_sync", None)
    payload_text = json.dumps(payload_snapshot, ensure_ascii=False, sort_keys=True, default=str)
    key_material = f"{method}@v{version}:{payload_text}"
    return hashlib.md5(key_material.encode("utf-8")).hexdigest()


def read_cached_response(cache_key: str) -> dict[str, Any] | None:
    entry = NEW_METHOD_CACHE.get(cache_key)
    if not entry:
        return None
    expires_at = float(entry.get("expires_at") or 0)
    if expires_at <= now_ts():
        NEW_METHOD_CACHE.pop(cache_key, None)
        return None
    cached = entry.get("value")
    if not isinstance(cached, dict):
        return None
    result = dict(cached)
    result["cache_hit"] = True
    result["cache_expires_at"] = expires_at
    return result


def write_cache_response(cache_key: str, payload: dict[str, Any], profile: str, response: Any) -> None:
    ttl = cache_ttl_for_profile(profile)
    NEW_METHOD_CACHE[cache_key] = {
        "expires_at": now_ts() + float(ttl),
        "value": response,
        "cache_key": cache_key,
        "cache_profile": profile,
    }


def _normalize_unified_response(
    method: str,
    request_id: str,
    payload: dict[str, Any],
    request_start: float,
    *,
    items: list[Any] | Any,
    status: str = "ok",
    warnings: list[str] | None = None,
    diagnostics: dict[str, Any] | None = None,
    cache_key: str | None = None,
    cache_profile: str = "normal",
    cache_hit: bool = False,
    total: int | None = None,
) -> dict[str, Any]:
    request_items = items if isinstance(items, list) else []
    if not isinstance(items, list):
        request_items = [items] if items is not None else []
    limit = coerce_int(payload.get("limit"), 50, minimum=1, maximum=500) or 50
    offset = coerce_int(payload.get("offset"), 0, minimum=0) or 0
    total_count = len(request_items) if total is None else int(total)
    paginated = request_items[offset : offset + limit]
    response = {
        "schema": f"adapter_1c_unified.{method}.v{NEW_API_VERSION}",
        "status": status,
        "request_id": request_id,
        "method": method,
        "latency_ms": int((now_ts() - request_start) * 1000),
        "items": paginated,
        "pagination": {
            "limit": limit,
            "offset": offset,
            "total": total_count,
            "has_more": offset + len(paginated) < total_count,
            "next_offset": offset + len(paginated) if offset + len(paginated) < total_count else None,
        },
        "warnings": warnings or [],
    }
    if diagnostics is not None:
        response["diagnostics"] = diagnostics
    expires_at = now_ts() + cache_ttl_for_profile(cache_profile)
    response["cache"] = {
        "cache_key": cache_key or make_cache_key(method, payload),
        "cache_hit": cache_hit,
        "cache_expires_at": int(expires_at),
        "cache_profile": cache_profile,
    }
    response["x_api_version"] = NEW_API_VERSION
    return response


def _as_list(value: Any) -> list[Any]:
    if value is None:
        return []
    return value if isinstance(value, list) else [value]


def _extract_json_items(raw: Any, *keys: str) -> list[Any]:
    if isinstance(raw, list):
        return raw
    if isinstance(raw, dict):
        for key in keys:
            if key in raw:
                nested = raw.get(key)
                if isinstance(nested, list):
                    return nested
        for key in ("result", "data", "rows", "values"):
            nested = raw.get(key)
            if isinstance(nested, list):
                return nested
            if isinstance(nested, dict):
                for item_key in ("items", "data", "values"):
                    values = nested.get(item_key)
                    if isinstance(values, list):
                        return values
    return []


def _coerce_limit_offset(payload: dict[str, Any], default_limit: int) -> tuple[int, int]:
    limit = coerce_int(payload.get("limit"), default_limit, minimum=1, maximum=500) or default_limit
    offset = coerce_int(payload.get("offset"), 0, minimum=0) or 0
    return limit, offset


def _add_warning(warnings: list[dict[str, Any]] | list[str], warning_code: str, message: str) -> None:
    if isinstance(warnings, list) and warnings and isinstance(warnings[0], dict):
        warnings.append({"code": warning_code, "message": message})
    elif isinstance(warnings, list):
        warnings.append(f"{warning_code}: {message}")


def _normalize_owner_dict(base_id: str, owner_payload: Any) -> dict[str, Any] | None:
    if not isinstance(owner_payload, dict):
        return None
    if owner_payload.get("status") == "resolved" and owner_payload.get("guid"):
        return {
            "status": owner_payload.get("status"),
            "kind": owner_payload.get("kind"),
            "name": owner_payload.get("name") or owner_payload.get("caption"),
            "synonym": owner_payload.get("synonym"),
            "guid": owner_payload.get("guid"),
            "source": "metadata.cache",
            "mechanism": owner_payload.get("mechanism", "unknown"),
        }
    owner_guid = owner_payload.get("guid")
    if not is_guid_text(owner_guid):
        owner_guid = owner_payload.get("id")
    if not is_guid_text(owner_guid):
        return None
    resolved = metadata_cache_lookup_identity(base_id, owner_guid)
    if not isinstance(resolved, dict):
        return {
            "status": "unknown",
            "kind": owner_payload.get("kind"),
            "name": owner_payload.get("name") or owner_payload.get("caption"),
            "guid": owner_guid,
            "source": "metadata.cache",
            "mechanism": owner_payload.get("mechanism", "unknown"),
        }
    return {
        "status": "resolved",
        "kind": resolved.get("kind") or owner_payload.get("kind"),
        "name": resolved.get("name") or owner_payload.get("name"),
        "synonym": resolved.get("synonym"),
        "guid": owner_guid,
        "source": "metadata.cache",
        "mechanism": owner_payload.get("mechanism", "unknown"),
    }


def _normalize_owner_output(
    base_id: str,
    owner_payload: Any,
    *,
    fallback_kind: str | None = None,
    fallback_name: str | None = None,
    fallback_guid: str | None = None,
) -> dict[str, Any] | None:
    fallback: dict[str, Any] = {}
    if fallback_kind:
        fallback["kind"] = fallback_kind
    if fallback_name:
        fallback["name"] = fallback_name
    if fallback_guid:
        fallback["guid"] = fallback_guid

    normalized = _normalize_owner_dict(base_id, owner_payload)
    if isinstance(normalized, dict):
        if not normalized.get("kind") and fallback.get("kind"):
            normalized["kind"] = fallback["kind"]
        if not normalized.get("name") and fallback.get("name"):
            normalized["name"] = fallback["name"]
        if not normalized.get("guid") and fallback.get("guid"):
            normalized["guid"] = fallback["guid"]
        if not normalized.get("source"):
            normalized["source"] = normalized.get("status") or "metadata.cache"
        return normalized

    if fallback:
        resolved_guid = fallback.get("guid")
        if resolved_guid and is_guid_text(resolved_guid):
            return {
                "status": "candidate",
                "kind": fallback.get("kind"),
                "name": fallback.get("name"),
                "guid": resolved_guid,
                "source": "selector",
                "mechanism": "fallback",
            }
        if fallback.get("name") or fallback.get("kind"):
            return {
                "status": "candidate",
                "kind": fallback.get("kind"),
                "name": fallback.get("name"),
                "source": "selector",
                "mechanism": "fallback",
            }
    return None


def _run_bulk_execute(payload: dict[str, Any], request_start: float, request_id: str) -> dict[str, Any]:
    cache_profile = normalize_cache_profile(payload)
    cache_key = make_cache_key("bulk.execute", payload)
    cached = read_cached_response(cache_key)
    if cached is not None:
        return cached

    base_id = str(payload.get("base_id") or "").strip()
    requests = _as_list(payload.get("requests"))
    if not is_guid_text(base_id):
        return _normalize_unified_response(
            "bulk.execute",
            request_id,
            payload,
            request_start,
            status="invalid_argument",
            items=[],
            diagnostics={"code": "invalid_argument", "message": "base_id required"},
            warnings=["base_id is required and must be GUID"],
            cache_key=cache_key,
            cache_profile=cache_profile,
            total=0,
            cache_hit=False,
        )
    if not requests:
        return _normalize_unified_response(
            "bulk.execute",
            request_id,
            payload,
            request_start,
            status="invalid_argument",
            items=[],
            diagnostics={"code": "invalid_argument", "message": "requests must be a non-empty array"},
            warnings=["no requests provided for bulk.execute"],
            cache_key=cache_key,
            cache_profile=cache_profile,
            total=0,
            cache_hit=False,
        )

    requests = requests[:BULK_MAX_REQUESTS]
    results: list[dict[str, Any]] = []
    for index, request in enumerate(requests):
        if not isinstance(request, dict):
            results.append({"index": index, "status": "invalid_argument", "error": "request must be an object"})
            continue
        submethod = str(request.get("method") or "").strip()
        subpayload = request.get("payload")
        if not isinstance(subpayload, dict):
            subpayload = {}
        if "base_id" not in subpayload and base_id:
            subpayload = dict(subpayload)
            subpayload["base_id"] = base_id
        try:
            sub_result = call_adapter_method(submethod, subpayload)
            if submethod == "modules.read":
                sub_result = enrich_modules_read_result(subpayload, sub_result)
            sub_result = maybe_enrich_owner_fields(submethod, subpayload if isinstance(subpayload, dict) else None, sub_result)
            summary: dict[str, Any] = {}
            if submethod == "code.search" and isinstance(sub_result, dict):
                search_items = _as_list(sub_result.get("items"))
                search_diagnostics = sub_result.get("diagnostics") if isinstance(sub_result.get("diagnostics"), dict) else {}
                search_types: list[str] = []
                for hit in search_items:
                    if isinstance(hit, dict):
                        owner = hit.get("owner") or hit.get("source_ref")
                        if isinstance(owner, dict):
                            kind = str(owner.get("kind") or "").strip()
                            if kind and kind not in search_types:
                                search_types.append(kind)
                summary = {
                    "status": str(sub_result.get("status") or "ok"),
                    "matches_count": len(search_items),
                    "total": sub_result.get("total", len(search_items)),
                    "query": search_diagnostics.get("query") if isinstance(search_diagnostics, dict) else None,
                    "regex": bool(search_diagnostics.get("regex")) if isinstance(search_diagnostics, dict) else False,
                    "scope": search_diagnostics.get("scope") if isinstance(search_diagnostics, dict) else None,
                    "object_type_filter": str(subpayload.get("object_type") or ""),
                    "owner_kinds": search_types[:12],
                    "warnings_count": len(_as_list(sub_result.get("warnings"))),
                }
            elif submethod == "metadata.resolve_overrides" and isinstance(sub_result, dict):
                override_items = _as_list(sub_result.get("items"))
                layer_counts: dict[str, int] = {}
                extension_count = 0
                for item in override_items:
                    if not isinstance(item, dict):
                        continue
                    layer = str(item.get("layer") or "").strip() or "unknown"
                    layer_counts[layer] = layer_counts.get(layer, 0) + 1
                    if item.get("extension") is not None:
                        extension_count += 1
                summary = {
                    "status": str(sub_result.get("status") or "ok"),
                    "override_count": len(override_items),
                    "extension_count": extension_count,
                    "layer_distribution": layer_counts,
                    "method_name": str(subpayload.get("method_name") or ""),
                    "base_object_name": str(subpayload.get("object_name") or subpayload.get("object_guid") or ""),
                    "warnings_count": len(_as_list(sub_result.get("warnings"))),
                }
            elif submethod == "templates.bindings" and isinstance(sub_result, dict):
                binding_items = _as_list(sub_result.get("items"))
                binding_count = 0
                template_names: list[str] = []
                for item in binding_items:
                    if not isinstance(item, dict):
                        continue
                    template = item.get("template", {})
                    if isinstance(template, dict):
                        name = str(template.get("name") or "").strip()
                        if name and name not in template_names:
                            template_names.append(name)
                    bindings = item.get("bindings") if isinstance(item.get("bindings"), list) else []
                    binding_count += len(_as_list(bindings))
                summary = {
                    "status": str(sub_result.get("status") or "ok"),
                    "templates_count": len(binding_items),
                    "bindings_count": binding_count,
                    "templates": template_names[:8],
                    "object_type": str(subpayload.get("object_type") or ""),
                    "object_name": str(subpayload.get("object_name") or ""),
                    "warnings_count": len(_as_list(sub_result.get("warnings"))),
                }
            elif submethod == "extensions.list" and isinstance(sub_result, dict):
                extension_items = _as_list(sub_result.get("items"))
                active_count = 0
                inactive_count = 0
                for item in extension_items:
                    if not isinstance(item, dict):
                        continue
                    is_inactive = (
                        truthy(item.get("is_disabled"))
                        or item.get("status") == "inactive"
                        or item.get("state") == "inactive"
                    )
                    if is_inactive:
                        inactive_count += 1
                    else:
                        active_count += 1
                summary = {
                    "status": str(sub_result.get("status") or "ok"),
                    "extensions_count": len(extension_items),
                    "active_count": active_count,
                    "inactive_count": inactive_count,
                    "with_load_order": sum(
                        1 for item in extension_items if isinstance(item, dict) and item.get("load_order") is not None
                    ),
                    "is_active_filter": subpayload.get("is_active"),
                    "limit": subpayload.get("limit"),
                    "offset": subpayload.get("offset"),
                    "warnings_count": len(_as_list(sub_result.get("warnings"))),
                }
            elif submethod == "code.read" and isinstance(sub_result, dict):
                read_items = _as_list(sub_result.get("items"))
                read_item = read_items[0] if read_items else {}
                if not isinstance(read_item, dict):
                    read_item = {}
                summary = {
                    "status": str(sub_result.get("status") or "ok"),
                    "source_length": len(str(read_item.get("text") or "")),
                    "has_source": bool(str(read_item.get("text") or "").strip()),
                    "module_ref": read_item.get("module_ref"),
                    "has_owner": isinstance(read_item.get("owner"), dict),
                    "owner_kind": read_item.get("owner", {}).get("kind") if isinstance(read_item.get("owner"), dict) else None,
                    "owner_name": read_item.get("owner", {}).get("name") if isinstance(read_item.get("owner"), dict) else None,
                    "owner_guid": read_item.get("owner", {}).get("guid") if isinstance(read_item.get("owner"), dict) else None,
                    "warnings_count": len(_as_list(sub_result.get("warnings"))),
                }
            elif submethod == "diagnostics.call_chain" and isinstance(sub_result, dict):
                call_chain_items = _as_list(sub_result.get("items"))
                call_chain_diagnostics = sub_result.get("diagnostics") if isinstance(sub_result.get("diagnostics"), dict) else {}
                call_chain_edges = call_chain_diagnostics.get("edges") if isinstance(call_chain_diagnostics, dict) else []
                summary = {
                    "status": str(sub_result.get("status") or "ok"),
                    "nodes_count": len(call_chain_items),
                    "edges_count": len(_as_list(call_chain_edges)),
                    "max_depth": call_chain_diagnostics.get("max_depth"),
                    "max_nodes": call_chain_diagnostics.get("max_nodes"),
                    "truncated": bool(call_chain_diagnostics.get("truncated", False)),
                    "entry_routine": call_chain_items[0].get("routine") if call_chain_items else None,
                    "last_routine": call_chain_items[-1].get("routine") if call_chain_items else None,
                    "warnings_count": len(_as_list(sub_result.get("warnings"))),
                }
            elif not summary and isinstance(sub_result, dict):
                result_items = _as_list(sub_result.get("items"))
                result_diagnostics = sub_result.get("diagnostics")
                if isinstance(result_diagnostics, dict):
                    diagnostics_hint = {
                        k: result_diagnostics[k]
                        for k in ("count", "total", "query", "max_depth", "max_nodes", "edges", "base_id")
                        if k in result_diagnostics
                    }
                else:
                    diagnostics_hint = {}
                summary = {
                    "status": str(sub_result.get("status") or "ok"),
                    "total": sub_result.get("total", len(result_items)),
                    "items_count": len(result_items),
                    "result_keys": list(sub_result.keys())[:24],
                    "diagnostics": diagnostics_hint,
                    "warnings_count": len(_as_list(sub_result.get("warnings"))),
                }
            results.append(
                {
                    "index": index,
                    "method": submethod,
                    "status": sub_result.get("status") if isinstance(sub_result, dict) else "ok",
                    "result": sub_result,
                    **({"summary": summary} if summary else {}),
                }
            )
        except AdapterError as exc:
            results.append({"index": index, "method": submethod, "status": "error", "error": str(exc), "diagnostics": adapter_error_result(submethod or "unknown", exc)})
        except Exception as exc:
            item = {"index": index, "method": submethod, "status": "error", "error": str(exc)}
            if truthy(os.environ.get("ONEC_MCP_DEBUG_DIAGNOSTICS")):
                item["traceback"] = traceback.format_exc(limit=5)
            results.append(item)

    requested_count = len(_as_list(payload.get("requests")))
    failed_count = len([item for item in results if (item.get("status") in {"error", "invalid_argument"})])
    ok_count = len([item for item in results if item.get("status") == "ok"])
    partial_status = "ok" if failed_count == 0 else ("error" if ok_count == 0 else "partial")
    result = _normalize_unified_response(
        "bulk.execute",
        request_id,
        payload,
        request_start,
        items=results,
        status=partial_status,
        warnings=[],
        diagnostics={
            "count": len(results),
            "requested": requested_count,
            "truncated": requested_count > len(requests),
            "ok_count": ok_count,
            "failed_count": failed_count,
        },
        cache_key=cache_key,
        cache_profile=cache_profile,
        total=len(results),
        cache_hit=False,
    )
    write_cache_response(cache_key, payload, cache_profile, result)
    return result


def _run_unified_method(method: str, payload: dict[str, Any], request_start: float, request_id: str) -> dict[str, Any]:
    if method == "bulk.execute":
        return _run_bulk_execute(payload, request_start, request_id)
    return _normalize_unified_response(
        method,
        request_id,
        payload,
        request_start,
        items=[],
        status="not_supported",
        diagnostics={"code": "method_not_supported", "message": f"method {method} is not registered as unified"},
        cache_profile=normalize_cache_profile(payload),
        cache_key=make_cache_key(method, payload),
        total=0,
        cache_hit=False,
    )


def enrich_modules_read_result(payload: dict[str, Any], result: Any) -> Any:
    if not isinstance(result, dict) or result.get("status") != "ok":
        return result
    if "owner" in result and isinstance(result.get("owner"), dict):
        enriched = dict(result)
        if not enriched.get("owner_display"):
            enriched["owner_display"] = _build_object_display_payload(result["owner"]) or {
                "kind": result["owner"].get("kind"),
                "name": result["owner"].get("name"),
                "guid": result["owner"].get("guid"),
            }
        return enriched
    return result


def build_freshness_context(payload: dict[str, Any]) -> dict[str, Any]:
    source_mode = normalize_source_mode(payload.get("source_mode"))
    source_state = normalize_source_state(payload.get("source_state"), source_mode)
    cache_policy = normalize_cache_policy(payload.get("cache_policy"), source_mode)
    force_refresh = truthy(payload.get("force_refresh"))
    snapshot_hint = payload.get("snapshot_hint")
    snapshot_token = None
    if isinstance(snapshot_hint, dict):
        snapshot_token = snapshot_hint.get("revision_token") if isinstance(snapshot_hint.get("revision_token"), str) else None
        if snapshot_token is None:
            snapshot_token = snapshot_hint.get("snapshot") if isinstance(snapshot_hint.get("snapshot"), str) else None
    elif isinstance(snapshot_hint, str):
        snapshot_token = snapshot_hint
    return {
        "source_mode": source_mode,
        "source_state": source_state,
        "cache_policy": cache_policy,
        "force_refresh": force_refresh,
        "snapshot_token": snapshot_token,
    }


def enrich_result_with_freshness(payload: dict[str, Any], method: str, result: Any, request_start: float) -> Any:
    if not isinstance(result, dict):
        return result
    context = build_freshness_context(payload)
    if method.startswith("storage."):
        context["cache_policy"] = "none"
        context["force_refresh"] = True
    context["request_id"] = str(payload.get("_mcp_request_id") or uuid.uuid4().hex)
    context["method"] = method
    context["base_id"] = payload.get("base_id")
    latency_ms = int((now_ts() - request_start) * 1000)
    context["latency_ms"] = latency_ms
    context["status"] = "fresh" if bool(context["force_refresh"] or context["cache_policy"] == "none") else "possibly_stale"
    warnings: list[str] = []
    if context["source_mode"] == "designer":
        if context["cache_policy"] == "ttl":
            warnings.append("designer mode with cache_policy=ttl can return stale data in live edit sessions.")
        if context["source_state"] != "working" and context["source_state"] != "all":
            warnings.append("designer mode should normally use source_state=working to include non-applied metadata.")
        if context["snapshot_token"] is None:
            warnings.append("snapshot_hint is not set; freshness is best-effort for designer mode.")
    if context["source_mode"] == "runtime" and context["cache_policy"] == "none":
        warnings.append("runtime mode requested cache_policy=none; additional metadata checks may be slower.")
    if warnings:
        context["warnings"] = warnings
    enriched = dict(result)
    enriched.setdefault("_freshness", {}).update(context)
    return enriched


def apply_freshness_request_policy(payload: dict[str, Any], method: str) -> dict[str, Any]:
    source_mode = normalize_source_mode(payload.get("source_mode"))
    if method == "code.write":
        source_mode = "designer"
    cache_policy = normalize_cache_policy(payload.get("cache_policy"), source_mode)
    explicit_source_state = normalize_source_state(payload.get("source_state"), source_mode) if payload.get("source_state") is not None else ""
    configuration_view, source_state = normalize_configuration_view(payload.get("configuration_view"), explicit_source_state)
    if not configuration_view:
        transformed = dict(payload)
        transformed["_configuration_view_error"] = "configuration_view conflicts with source_state or is unsupported."
        return transformed
    if not source_state:
        source_state = normalize_source_state(None, source_mode)
        configuration_view, source_state = normalize_configuration_view(payload.get("configuration_view"), source_state)
    if method == "code.write":
        source_state = "working"
        cache_policy = "none"
    force_refresh = truthy(payload.get("force_refresh"))
    if method.startswith("storage."):
        # Storage methods always call the live SQL layer or adapter-local
        # backup store directly; their result is never served from the
        # metadata/vector cache.
        cache_policy = "none"
        force_refresh = True
    transformed = dict(payload)
    transformed["source_mode"] = source_mode
    transformed["source_state"] = source_state
    transformed["configuration_view"] = configuration_view
    transformed["cache_policy"] = cache_policy
    if method in REST_STATE_METHODS and "state" not in transformed:
        transformed["state"] = REST_STATE_BY_SOURCE_STATE.get(source_state, "working")
    if force_refresh:
        transformed["force_refresh"] = True
    if source_mode == "designer" and method in {
        "metadata.objects.list",
        "metadata.object.get",
        "metadata.object.full",
        "metadata.definition.find",
        "metadata.resolve_overrides",
        "metadata.route.resolve",
        "code.search",
        "code.read",
        "modules.search",
        "templates.bindings",
        "templates.read",
        "templates.analyze",
        "templates.map",
        "extension.objects.find",
        "diagnostics.call_chain",
    }:
        if "refresh_cache" not in transformed:
            transformed["refresh_cache"] = bool(force_refresh)
        if cache_policy == "none":
            transformed["refresh_cache"] = True if force_refresh else transformed.get("refresh_cache", True)
    return transformed


def mcp_audit_event(event: dict[str, Any]) -> None:
    """Persist proxy telemetry without BSL text, payload bytes, or credentials."""
    try:
        path = Path(os.environ.get("ONEC_MCP_AUDIT_LOG_PATH") or "/data/mcp-audit.jsonl")
        path.parent.mkdir(parents=True, exist_ok=True)
        with MCP_AUDIT_LOCK:
            max_bytes = max(1_048_576, int(os.environ.get("ONEC_MCP_AUDIT_MAX_BYTES") or 52_428_800))
            keep_files = max(1, min(20, int(os.environ.get("ONEC_MCP_AUDIT_KEEP_FILES") or 10)))
            if path.exists() and path.stat().st_size >= max_bytes:
                for index in range(keep_files - 1, 0, -1):
                    source = path.with_name(f"{path.name}.{index}")
                    target = path.with_name(f"{path.name}.{index + 1}")
                    if source.exists():
                        source.replace(target)
                path.replace(path.with_name(f"{path.name}.1"))
            with path.open("a", encoding="utf-8") as handle:
                handle.write(json.dumps(event, ensure_ascii=False, sort_keys=True, default=str) + "\n")
    except (OSError, ValueError):
        return


def mcp_audit_request_summary(payload: dict[str, Any]) -> dict[str, Any]:
    keys = ("base_id", "extension", "extension_guid", "ref", "kind", "name", "object_type", "object_name", "module_ordinal", "mode")
    return {key: payload.get(key) for key in keys if payload.get(key) not in {None, ""}}


def http_json(
    method: str,
    path: str,
    payload: dict[str, Any] | None = None,
    timeout: float | None = None,
    request_id: str | None = None,
) -> Any:
    url = f"{adapter_url()}{path}"
    data = None if payload is None else json.dumps(payload, ensure_ascii=False).encode("utf-8")
    headers = {"Accept": "application/json"}
    if data is not None:
        headers["Content-Type"] = "application/json; charset=utf-8"
    if adapter_token():
        headers["Authorization"] = f"Bearer {adapter_token()}"
    if request_id and re.fullmatch(r"[A-Za-z0-9_.-]{8,128}", request_id):
        headers["X-Request-ID"] = request_id
    request = urllib.request.Request(url, data=data, headers=headers, method=method)
    effective_timeout = adapter_timeout() if timeout is None else timeout
    try:
        with urllib.request.urlopen(request, timeout=effective_timeout) as response:
            raw = response.read().decode("utf-8-sig")
            return json.loads(raw) if raw.strip() else {"status": response.status}
    except TimeoutError as exc:
        raise AdapterError(f"REST adapter request timed out after {effective_timeout} seconds") from exc
    except urllib.error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        raise AdapterError(f"REST adapter returned HTTP {exc.code}", status=exc.code, body=body) from exc
    except (urllib.error.URLError, http.client.HTTPException, OSError) as exc:
        reason = getattr(exc, "reason", None) or str(exc) or type(exc).__name__
        raise AdapterError(f"REST adapter is unavailable: {reason}") from exc


def call_adapter_method(method: str, payload: dict[str, Any], *, timeout: float | None = None) -> Any:
    request_id = str(payload.get("_mcp_request_id") or "").strip()
    started = now_ts()
    try:
        if method == "health":
            query = ""
            if payload.get("base_id"):
                query = "?" + urllib.parse.urlencode({"base_id": str(payload.get("base_id"))})
            result = http_json("GET", f"/health{query}", timeout=timeout, request_id=request_id)
        elif method == "help.methods":
            try:
                result = http_json("POST", "/rpc", {"method": method, "payload": payload}, timeout=timeout, request_id=request_id)
            except AdapterError as exc:
                if exc.status not in {404, 405}:
                    raise
                result = http_json("GET", "/methods", timeout=timeout, request_id=request_id)
        else:
            result = http_json("POST", "/rpc", {"method": method, "payload": payload}, timeout=timeout, request_id=request_id)
    except Exception as exc:
        mcp_audit_event({
            "event": "mcp_adapter_call", "time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
            "request_id": request_id or None, "method": method, "request": mcp_audit_request_summary(payload),
            "status": "exception", "error": "adapter_unavailable" if isinstance(exc, AdapterError) else "mcp_request_exception",
            "exception_type": type(exc).__name__, "duration_ms": int((now_ts() - started) * 1000),
        })
        raise
    mcp_audit_event({
        "event": "mcp_adapter_call", "time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
        "request_id": request_id or None, "method": method, "request": mcp_audit_request_summary(payload),
        "status": result.get("status") if isinstance(result, dict) else None,
        "error": result.get("error") if isinstance(result, dict) else None,
        "duration_ms": int((now_ts() - started) * 1000),
    })
    return result


def public_error(method: str, error: str, diagnostics: Any | None = None, *, schema: str = "adapter_1c_mcp_error.v1") -> dict[str, Any]:
    safe_diagnostics = diagnostics if diagnostics is not None else {"message": error}
    if not truthy(os.environ.get("ONEC_MCP_DEBUG_DIAGNOSTICS")):
        safe_diagnostics = strip_private_error_diagnostics(safe_diagnostics)
    return {
        "schema": schema,
        "status": "error",
        "method": method,
        "error": error,
        "diagnostics": safe_diagnostics,
    }


def strip_private_error_diagnostics(value: Any) -> Any:
    if isinstance(value, list):
        return [strip_private_error_diagnostics(item) for item in value]
    if not isinstance(value, dict):
        return value
    return {
        key: strip_private_error_diagnostics(item)
        for key, item in value.items()
        if key not in {"traceback", "stack", "stacktrace", "exception_repr"}
    }


def invalid_argument(method: str, argument: str, message: str, *, allowed_values: list[str] | None = None) -> dict[str, Any]:
    return {
        "schema": "onec_adapter_request_error.v1",
        "status": "invalid_argument",
        "method": method,
        "error": "invalid_argument",
        "argument": argument,
        "diagnostics": {"message": message},
        **({"allowed_values": allowed_values} if allowed_values else {}),
    }


def validate_metadata_object_full_sections(payload: dict[str, Any]) -> dict[str, Any] | None:
    if "sections" not in payload:
        return None
    sections = payload.get("sections")
    if not isinstance(sections, list):
        return invalid_argument(
            "metadata.object.full",
            "sections",
            "sections must be a JSON array of section names.",
            allowed_values=sorted(FULL_METHOD_SECTIONS | {FULL_METHOD_ALL_KEY}),
        )
    if not sections:
        return invalid_argument(
            "metadata.object.full",
            "sections",
            "sections must contain at least one section name.",
            allowed_values=sorted(FULL_METHOD_SECTIONS | {FULL_METHOD_ALL_KEY}),
        )

    requested_sections: list[str] = []
    for section in sections:
        if not isinstance(section, str):
            return invalid_argument(
                "metadata.object.full",
                "sections",
                "sections must be a JSON array of section names.",
                allowed_values=sorted(FULL_METHOD_SECTIONS | {FULL_METHOD_ALL_KEY}),
            )
        section_name = section.strip().lower()
        if section_name == FULL_METHOD_ALL_KEY:
            for candidate in FULL_METHOD_SECTION_ORDER:
                if candidate not in requested_sections:
                    requested_sections.append(candidate)
            continue
        if section_name not in FULL_METHOD_SECTIONS:
            return invalid_argument(
                "metadata.object.full",
                "sections",
                f"Unsupported section `{section}`.",
                allowed_values=sorted(FULL_METHOD_SECTIONS | {FULL_METHOD_ALL_KEY}),
            )
        if section_name not in requested_sections:
            requested_sections.append(section_name)
    payload["sections"] = requested_sections
    return None


def adapter_error_result(method: str, exc: AdapterError) -> dict[str, Any]:
    body_json = None
    if exc.body:
        try:
            body_json = json.loads(exc.body)
        except Exception:
            body_json = exc.body[:4000]
    return public_error(
        method,
        "adapter_unavailable" if exc.status is None else "adapter_http_error",
        {
            "message": str(exc),
            "http_status": exc.status,
            "body": body_json,
        },
    )


def now_ts() -> float:
    return time.time()


def job_set(job_id: str, **updates: Any) -> None:
    with JOB_LOCK:
        job = JOBS.get(job_id)
        if job:
            job.update(updates)
            job["updated_at"] = now_ts()


def job_snapshot(job_id: str) -> dict[str, Any]:
    with JOB_LOCK:
        return dict(JOBS.get(job_id) or {})


def job_cancel_requested(job_id: str) -> bool:
    with JOB_LOCK:
        job = JOBS.get(job_id) or {}
        return truthy(job.get("cancel_requested")) or job.get("status") == "cancelled"


def job_finish(job_id: str, status: str, **updates: Any) -> None:
    with JOB_LOCK:
        job = JOBS.get(job_id)
        if not job:
            return
        if job.get("status") == "cancelled" and status != "cancelled":
            return
        job.update(updates)
        job["status"] = status
        job["finished_at"] = now_ts()
        job["updated_at"] = job["finished_at"]


def job_heartbeat(job_id: str, stop_event: threading.Event) -> None:
    while not stop_event.wait(job_heartbeat_seconds()):
        with JOB_LOCK:
            job = JOBS.get(job_id)
            if not job or job.get("status") not in {"queued", "running"}:
                return
            job["updated_at"] = now_ts()
            progress = dict(job.get("progress") or {})
            progress["heartbeat_at"] = job["updated_at"]
            job["progress"] = progress


def cancel_adapter_job(job_id: str) -> dict[str, Any]:
    cleanup_jobs()
    with JOB_LOCK:
        job = JOBS.get(job_id)
        if not job:
            return {
                "schema": "adapter_1c_mcp_job.v1",
                "status": "not_found",
                "job_id": job_id,
                "diagnostics": {"message": "Job was not found. It may have expired or the MCP proxy was restarted."},
            }
        if job.get("status") in {"done", "error", "timeout", "cancelled"}:
            return dict(job)
        job["cancel_requested"] = True
        job["status"] = "cancelled"
        job["updated_at"] = now_ts()
        job["finished_at"] = job["updated_at"]
        job["diagnostics"] = {"message": "Cancellation requested. A running REST request may finish in the background, but this job will remain cancelled."}
        return dict(job)


def full_partial_result(base_id: str | None = None, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    query = payload or {}
    return {
        "schema": "onec_metadata_object_full.v1",
        "status": "partial",
        "base_id": base_id or query.get("base_id"),
        "source": {"kind": "live_metadata"},
        "query": {
            "guid": query.get("guid"),
            "kind": query.get("kind"),
            "name": query.get("name"),
            **({"ordinal": query.get("ordinal")} if query.get("ordinal") not in {None, ""} else {}),
            "include_storage": truthy(query.get("include_storage")),
        },
        "sections": {
            "card": "pending",
            "semantic": "pending",
            "forms": "pending",
            "templates": "pending",
            "commands": "pending",
            "modules": "pending",
            "parts_summary": "not_requested",
        },
        "failed_sections": [],
        "diagnostics": [],
        "counts": {},
    }


def merge_section_counts(partial: dict[str, Any]) -> None:
    forms = partial.get("forms") or []
    templates = partial.get("templates") or []
    commands = partial.get("commands") or []
    modules = partial.get("modules") or []
    semantic_sections = ((partial.get("semantic") or {}).get("sections") or []) if isinstance(partial.get("semantic"), dict) else []
    partial["counts"] = {
        "forms": len(forms),
        "templates": len(templates),
        "commands": len(commands),
        "modules": len(modules),
        "attributes": sum(int((section or {}).get("declared_record_count") or 0) for section in semantic_sections if (section or {}).get("category") == "Attribute"),
        "tabular_sections": sum(int((section or {}).get("declared_record_count") or 0) for section in semantic_sections if (section or {}).get("category") == "TabularSection"),
    }


def update_full_partial(job_id: str, partial: dict[str, Any], current_step: str, completed: int, total: int) -> None:
    merge_section_counts(partial)
    job_set(
        job_id,
        partial_result=partial,
        current_step=current_step,
        progress={
            "current_step": current_step,
            "completed_steps": completed,
            "total_steps": total,
            "percent": int((completed / total) * 100) if total else 0,
        },
    )


def section_failed(partial: dict[str, Any], section: str, method: str, result: Any) -> None:
    status = result.get("status") if isinstance(result, dict) else "error"
    diagnostics = result.get("diagnostics") if isinstance(result, dict) else {"message": str(result)}
    partial["sections"][section] = "failed"
    if any(item.get("section") == section for item in partial.get("failed_sections") or []):
        return
    partial.setdefault("failed_sections", []).append(
        {"section": section, "method": method, "status": status, "diagnostics": diagnostics}
    )
    partial.setdefault("diagnostics", []).append(
        {"section": section, "method": method, "status": status, "diagnostics": diagnostics}
    )


def section_ok_or_empty(partial: dict[str, Any], section: str, value: Any) -> None:
    partial["sections"][section] = "ok" if value else "empty"


def run_metadata_object_full_job(job_id: str, method: str, payload: dict[str, Any], timeout_seconds: float) -> None:
    base_id = str(payload.get("base_id") or "")
    partial = full_partial_result(base_id, payload)
    started_at = now_ts()
    requested_sections_raw: list[str] = [str(section) for section in (payload.get("_sections") or payload.get("sections") or []) if isinstance(section, str)]
    requested_sections = []
    for section_name in requested_sections_raw:
        normalized = section_name.strip().lower()
        if normalized == FULL_METHOD_ALL_KEY:
            for candidate in FULL_METHOD_SECTION_ORDER:
                if candidate not in requested_sections:
                    requested_sections.append(candidate)
            continue
        if normalized in FULL_METHOD_SECTIONS and normalized not in requested_sections:
            requested_sections.append(normalized)
    if not requested_sections:
        requested_sections = ["card", "semantic", "modules", "templates", "forms", "commands"]
        if truthy(payload.get("include_parts_summary")) or truthy(payload.get("include_storage")):
            requested_sections.append("parts_summary")
    elif "parts_summary" not in requested_sections and (truthy(payload.get("include_parts_summary")) or truthy(payload.get("include_storage"))):
        requested_sections.append("parts_summary")
    for section in requested_sections:
        if section not in partial.get("sections", {}):
            partial.setdefault("sections", {})[section] = "not_requested"
    steps: list[tuple[str, str, dict[str, Any]]] = []
    for section in requested_sections:
        if section == "card":
            steps.append(("card", "metadata.object.get", {**payload, "include_semantic": False}))
        elif section == "semantic":
            steps.append(("semantic", "metadata.object.get", payload))
        elif section == "forms":
            steps.append(
                (
                    "forms",
                    "metadata.object.form.details",
                    {
                        **payload,
                        "max_items": int(payload.get("max_form_items") or payload.get("max_items") or 1000),
                        "max_forms": int(payload.get("max_forms") or 20),
                        "include_module_text": truthy(payload.get("include_form_module_text")),
                    },
                )
            )
        elif section == "templates":
            steps.append(
                (
                    "templates",
                    "metadata.object.template.details" if truthy(payload.get("include_template_details")) else "metadata.object.templates",
                    payload,
                )
            )
        elif section == "commands":
            steps.append(("commands", "metadata.object.commands", payload))
        elif section == "modules":
            steps.append(("modules", "metadata.object.modules", payload))
        elif section == "parts_summary":
            steps.append(("parts_summary", "metadata.object.parts", {**payload, "include_text": False, "include_tree": False}))
            partial["sections"]["parts_summary"] = "pending"
    total = len(steps)
    completed = 0
    running_steps = [section for section, _, _ in steps]
    update_full_partial(job_id, partial, "starting", completed, total)

    def run_section(section: str, section_method: str, section_payload: dict[str, Any]) -> tuple[str, str, Any]:
        try:
            elapsed = now_ts() - started_at
            remaining = max(1.0, timeout_seconds - elapsed)
            section_timeout = min(float(section_payload.get("timeout_seconds") or remaining), remaining)
            result = call_adapter_method(section_method, {**section_payload, "timeout_seconds": section_timeout}, timeout=section_timeout)
        except AdapterError as exc:
            result = adapter_error_result(section_method, exc)
        except Exception as exc:
            result = public_error(section_method, "mcp_section_exception", {"message": str(exc), "traceback": traceback.format_exc(limit=6)})
        return section, section_method, result

    executor = concurrent.futures.ThreadPoolExecutor(max_workers=max(1, total), thread_name_prefix=f"onec-full-{job_id[:8]}")
    futures = {executor.submit(run_section, section, section_method, section_payload): (section, section_method) for section, section_method, section_payload in steps}
    try:
        while futures:
            if job_cancel_requested(job_id):
                partial["status"] = "cancelled"
                executor.shutdown(wait=False, cancel_futures=True)
                job_finish(job_id, "cancelled", partial_result=partial, result=partial)
                return
            elapsed = now_ts() - started_at
            if elapsed >= timeout_seconds:
                for _, (section, section_method) in list(futures.items()):
                    section_failed(partial, section, section_method, {"status": "timeout", "diagnostics": {"message": f"MCP job timeout after {timeout_seconds:.0f} seconds"}})
                partial["status"] = "partial"
                executor.shutdown(wait=False, cancel_futures=True)
                job_finish(
                    job_id,
                    "error",
                    error="job_timeout",
                    partial_result=partial,
                    result=partial,
                    progress={"current_step": "timeout", "running_steps": running_steps, "completed_steps": completed, "total_steps": total, "percent": int((completed / total) * 100) if total else 0},
                    diagnostics={"message": f"MCP job timeout after {timeout_seconds:.0f} seconds", "running_steps": running_steps},
                )
                return
            done, _ = concurrent.futures.wait(futures, timeout=0.5, return_when=concurrent.futures.FIRST_COMPLETED)
            if not done:
                job_set(
                    job_id,
                    partial_result=partial,
                    current_step=",".join(running_steps) if running_steps else "waiting",
                    progress={
                        "current_step": "running_sections",
                        "running_steps": running_steps,
                        "completed_steps": completed,
                        "total_steps": total,
                        "percent": int((completed / total) * 100) if total else 0,
                    },
                )
                continue
            for future in done:
                section, section_method = futures.pop(future)
                if section in running_steps:
                    running_steps.remove(section)
                completed += 1
                try:
                    section, section_method, result = future.result()
                except Exception as exc:
                    result = public_error(section_method, "mcp_section_exception", {"message": str(exc), "traceback": traceback.format_exc(limit=6)})
                if not isinstance(result, dict) or result.get("status") not in {"ok", "partial"}:
                    section_failed(partial, section, section_method, result)
                elif section == "semantic":
                    partial["object"] = result.get("object")
                    partial["semantic"] = result.get("semantic")
                    section_ok_or_empty(partial, section, partial.get("semantic"))
                elif section == "forms":
                    partial["forms"] = result.get("forms") or []
                    section_ok_or_empty(partial, section, partial["forms"])
                elif section == "templates":
                    partial["templates"] = result.get("templates") or []
                    section_ok_or_empty(partial, section, partial["templates"])
                elif section == "commands":
                    partial["commands"] = result.get("commands") or []
                    section_ok_or_empty(partial, section, partial["commands"])
                elif section == "modules":
                    partial["modules"] = result.get("modules") or []
                    section_ok_or_empty(partial, section, partial["modules"])
                elif section == "parts_summary":
                    partial["parts_summary"] = {"counts": result.get("counts")}
                    partial["sections"][section] = "ok"
                update_full_partial(job_id, partial, section, completed, total)
    finally:
        executor.shutdown(wait=False, cancel_futures=True)
    partial["status"] = "partial" if partial.get("failed_sections") else "ok"
    merge_section_counts(partial)
    job_finish(job_id, "done", result=partial, partial_result=partial, progress={"current_step": "done", "completed_steps": total, "total_steps": total, "percent": 100})


def cleanup_jobs(now: float | None = None) -> None:
    effective_now = time.time() if now is None else now
    with JOB_LOCK:
        expired = [
            job_id
            for job_id, job in JOBS.items()
            if effective_now - float(job.get("updated_at") or job.get("created_at") or effective_now) > JOB_TTL_SECONDS
        ]
        for job_id in expired:
            JOBS.pop(job_id, None)


def start_adapter_job(method: str, payload: dict[str, Any]) -> str:
    cleanup_jobs()
    job_id = uuid.uuid4().hex
    now = time.time()
    timeout_seconds = job_timeout_seconds(payload)
    with JOB_LOCK:
        JOBS[job_id] = {
            "schema": "adapter_1c_mcp_job.v1",
            "job_id": job_id,
            "status": "queued",
            "method": method,
            "created_at": now,
            "updated_at": now,
            "timeout_seconds": timeout_seconds,
            "progress": {"current_step": "queued", "completed_steps": 0, "total_steps": None, "percent": 0},
        }

    def worker() -> None:
        stop_heartbeat = threading.Event()
        heartbeat_thread = threading.Thread(target=job_heartbeat, args=(job_id, stop_heartbeat), name=f"onec-heartbeat-{job_id[:8]}", daemon=True)
        heartbeat_thread.start()
        with JOB_LOCK:
            job = JOBS.get(job_id)
            if job:
                job["status"] = "running"
                job["started_at"] = time.time()
                job["updated_at"] = job["started_at"]
                job["progress"] = {"current_step": "running", "completed_steps": 0, "total_steps": None, "percent": 0}
        try:
            if method == "metadata.object.full":
                run_metadata_object_full_job(job_id, method, payload, timeout_seconds)
                return
            result = call_adapter_method(method, payload, timeout=timeout_seconds)
            if job_cancel_requested(job_id):
                job_finish(job_id, "cancelled", result={"status": "cancelled", "method": method})
                return
            job_finish(job_id, "done", result=result, progress={"current_step": "done", "completed_steps": 1, "total_steps": 1, "percent": 100})
        except AdapterError as exc:
            job_finish(job_id, "error", **adapter_error_result(method, exc))
        except Exception as exc:
            job_finish(
                job_id,
                "error",
                **public_error(
                    method,
                    "mcp_job_exception",
                    {"message": str(exc), "traceback": traceback.format_exc(limit=8)},
                    schema="adapter_1c_mcp_job.v1",
                ),
            )
        finally:
            stop_heartbeat.set()
            heartbeat_thread.join(timeout=0.2)

    def timeout_watchdog() -> None:
        time.sleep(timeout_seconds)
        with JOB_LOCK:
            job = JOBS.get(job_id)
            if not job or job.get("status") not in {"queued", "running"}:
                return
            partial = job.get("partial_result")
            current_step = ((job.get("progress") or {}).get("current_step") or job.get("current_step") or "running")
            running_steps = (job.get("progress") or {}).get("running_steps") or [current_step]
            job.update(
                public_error(
                    method,
                    "job_timeout",
                    {"message": f"MCP job timeout after {timeout_seconds:.0f} seconds", "current_step": current_step},
                    schema="adapter_1c_mcp_job.v1",
                )
            )
            if partial:
                if isinstance(partial, dict):
                    partial["status"] = "partial"
                    for section in running_steps:
                        section_name = str(section or "").strip() or current_step
                        if section_name == "running_sections":
                            continue
                        partial.setdefault("sections", {})[section_name] = "failed"
                        if not any(item.get("section") == section_name for item in partial.get("failed_sections") or []):
                            partial.setdefault("failed_sections", []).append(
                                {
                                    "section": section_name,
                                    "method": method,
                                    "status": "timeout",
                                    "diagnostics": {"message": f"MCP job timeout after {timeout_seconds:.0f} seconds"},
                                }
                            )
                job["partial_result"] = partial
                job["result"] = partial
            job["status"] = "error"
            job["finished_at"] = now_ts()
            job["updated_at"] = job["finished_at"]

    thread = threading.Thread(target=worker, name=f"onec-job-{job_id[:8]}", daemon=True)
    thread.start()
    watchdog = threading.Thread(target=timeout_watchdog, name=f"onec-timeout-{job_id[:8]}", daemon=True)
    watchdog.start()
    return job_id


def get_adapter_job(job_id: str, *, consume: bool = False) -> dict[str, Any]:
    cleanup_jobs()
    with JOB_LOCK:
        job = dict(JOBS.get(job_id) or {})
        if consume and job.get("status") in {"done", "error", "timeout", "cancelled", "not_found"}:
            JOBS.pop(job_id, None)
    if not job:
        return {
            "schema": "adapter_1c_mcp_job.v1",
            "status": "not_found",
            "job_id": job_id,
            "diagnostics": {"message": "Job was not found. It may have expired or the MCP proxy was restarted."},
        }
    return job


def method_requires_base_id(method: str) -> bool:
    if method in BASE_ID_OPTIONAL_METHODS:
        return False
    return method.startswith(BASE_ID_REQUIRED_METHOD_PREFIXES)


def missing_base_id_policy(method: str) -> dict[str, Any]:
    return {
        "schema": "adapter_1c_mcp_policy.v1",
        "status": "blocked",
        "method": method,
        "reason": "base_id_required",
        "diagnostics": {
            "message": (
                "This adapter method reads a concrete 1C database and requires payload.base_id. "
                "Do not guess a base id from examples. Use the project/user context or ask for the target base id. "
                "You may call onec_health with a known concrete base_id to check it before metadata/modules requests."
            ),
            "agent_guidance": [
                "If the task context already contains module_ref/read_selector, retry the direct read with the same base_id.",
                "If base_id is unknown, stop and ask for it instead of running metadata/modules searches.",
                "Treat not_found from scoped searches as method-scope evidence, not proof that code is absent.",
            ],
        },
    }


def metadata_write_code_guardrail(method: str, payload: dict[str, Any]) -> dict[str, Any] | None:
    if method not in {"metadata.write", "metadata.module.write_apply"} or truthy(payload.get("_allow_low_level_code_write")):
        return None
    target = payload.get("target") if isinstance(payload.get("target"), dict) else {}
    target_kind = str(target.get("kind") or payload.get("target_kind") or "").strip().lower()
    code_fields = ("routine_text", "module_text", "full_text", "code", "old", "new")
    has_code_edit = any(payload.get(field) is not None for field in code_fields)
    if not has_code_edit or target_kind not in {"module", "bsl_module", "bsl"}:
        return None
    owner_object_type = (
        payload.get("object_type")
        or target.get("object_type")
        or payload.get("owner_kind")
        or target.get("owner_kind")
    )
    if str(owner_object_type or "").strip().casefold() in {"module", "bsl_module", "bsl", "модуль"}:
        owner_object_type = None
    owner_object_name = (
        payload.get("object_name")
        or target.get("object_name")
        or payload.get("owner_name")
        or target.get("owner_name")
    )
    owner_object_guid = (
        payload.get("object_guid")
        or target.get("object_guid")
        or payload.get("owner_guid")
        or target.get("owner_guid")
    )
    suggested_payload = {
        "base_id": payload.get("base_id"),
        **{
            key: value
            for key, value in {
                "ref": payload.get("ref") or target.get("ref"),
                "module_ref": payload.get("module_ref") or target.get("module_ref"),
                "object_type": owner_object_type,
                "object_name": owner_object_name,
                "object_guid": owner_object_guid,
                "routine_name": payload.get("routine_name") or target.get("routine_name"),
                "routine_text": payload.get("routine_text"),
                "module_text": payload.get("module_text"),
                "full_text": payload.get("full_text"),
                "code": payload.get("code"),
                "old": payload.get("old"),
                "new": payload.get("new"),
                "mode": payload.get("mode") or "apply",
            }.items()
            if value is not None
        },
    }
    return {
        "schema": "adapter_1c_mcp_policy.v1",
        "status": "blocked",
        "method": method,
        "reason": "use_code_write_for_bsl",
        "diagnostics": {
            "message": "BSL edits through MCP must use code.write. code.write accepts 1C selectors and saves to the saved-state working layer without SQL/save-gate questions.",
            "suggested_request": {"method": "code.write", "payload": suggested_payload},
            "bypass": "Pass _allow_low_level_code_write=true only for explicit low-level adapter diagnostics.",
        },
    }


def purge_expired_selector_tokens() -> None:
    cutoff = now_ts() - SELECTOR_TOKEN_TTL_SECONDS
    with SELECTOR_TOKEN_LOCK:
        expired = [token for token, entry in SELECTOR_TOKENS.items() if float(entry.get("created_at") or 0) < cutoff]
        for token in expired:
            SELECTOR_TOKENS.pop(token, None)


def issue_selector_token(selector: dict[str, Any]) -> str:
    purge_expired_selector_tokens()
    token = f"onecsel_{uuid.uuid4().hex}"
    with SELECTOR_TOKEN_LOCK:
        SELECTOR_TOKENS[token] = {"created_at": now_ts(), "selector": dict(selector)}
    return token


def diagnostic_mode_authorized(payload: dict[str, Any]) -> bool:
    """Developer diagnostics are opt-in at deployment level, not an agent choice."""
    return (
        (truthy(payload.get("diagnostic")) or truthy(payload.get("_allow_diagnostic")))
        and truthy(os.environ.get("ONEC_MCP_ALLOW_DIAGNOSTIC"))
    )


def publicize_read_selectors(value: Any) -> Any:
    """Replace adapter-issued technical continuations with short-lived opaque tokens."""
    if isinstance(value, list):
        return [publicize_read_selectors(item) for item in value]
    if not isinstance(value, dict):
        return value
    public: dict[str, Any] = {}
    for key, item in value.items():
        if key in TECHNICAL_AGENT_FIELDS:
            continue
        if key == "read_selector" and isinstance(item, dict) and str(item.get("method") or "").strip():
            public[key] = {"method": str(item["method"]), "selector_token": issue_selector_token(item)}
        elif key == "read_selectors" and isinstance(item, dict):
            public[key] = {
                name: (
                    {"method": str(selector["method"]), "selector_token": issue_selector_token(selector)}
                    if isinstance(selector, dict) and str(selector.get("method") or "").strip()
                    else publicize_read_selectors(selector)
                )
                for name, selector in item.items()
            }
        else:
            public[key] = publicize_read_selectors(item)
    return public


def resolve_selector_token(method: str, payload: dict[str, Any]) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
    token = str(payload.get("selector_token") or "").strip()
    if not token:
        return payload, None
    purge_expired_selector_tokens()
    with SELECTOR_TOKEN_LOCK:
        entry = SELECTOR_TOKENS.get(token)
    selector = entry.get("selector") if isinstance(entry, dict) and isinstance(entry.get("selector"), dict) else None
    if not selector:
        return None, public_error(method, "selector_token_invalid", {"message": "selector_token is unknown or expired; repeat the public discovery call."})
    selector_method = str(selector.get("method") or "").strip()
    if selector_method != method:
        return None, public_error(method, "selector_token_method_mismatch", {"message": f"selector_token is valid only for `{selector_method}`."})
    explicit = {key: value for key, value in payload.items() if key != "selector_token"}
    resolved = {**selector, **explicit, "_selector_token_resolved": True}
    return resolved, None


def technical_selector_fields(payload: Any) -> list[str]:
    """Find technical selector keys at every JSON level supplied by an agent."""
    found: set[str] = set()
    if isinstance(payload, dict):
        for key, value in payload.items():
            if key in TECHNICAL_AGENT_FIELDS:
                found.add(key)
            found.update(technical_selector_fields(value))
    elif isinstance(payload, list):
        for value in payload:
            found.update(technical_selector_fields(value))
    return sorted(found)


def normal_agent_technical_field_guardrail(method: str, payload: dict[str, Any]) -> dict[str, Any] | None:
    if diagnostic_mode_authorized(payload) or truthy(payload.get("_selector_token_resolved")):
        return None
    prohibited = technical_selector_fields(payload)
    if not prohibited:
        return None
    return {
        "schema": "adapter_1c_mcp_policy.v1",
        "status": "blocked",
        "method": method,
        "reason": "technical_selector_forbidden",
        "diagnostics": {
            "fields": prohibited,
            "message": "Use complete public 1C names (extension + ref + child name) or an adapter-issued selector_token. SQL/storage coordinates are developer diagnostics only.",
            "suggested_request": {
                "method": "metadata.object.full",
                "payload": {"base_id": payload.get("base_id"), "ref": payload.get("ref"), "configuration_view": "effective_working"},
            },
        },
    }


def runtime_form_inspection_unsupported(method: str, payload: dict[str, Any]) -> dict[str, Any] | None:
    if method not in {"runtime.form.elements.inspect", "runtime.form.inspect"}:
        return None
    return {
        "schema": "onec_runtime_form_inspection.v1",
        "status": "unsupported",
        "method": method,
        "error": "runtime_inspection_unsupported",
        "base_id": payload.get("base_id"),
        "diagnostics": {
            "message": "The SQL-only adapter does not open 1C forms, execute form handlers, or inspect runtime-generated controls. Read static metadata with metadata.form.decode; obtain runtime evidence through a separately authorised human-operated channel.",
        },
    }


def run_or_enqueue_adapter_method(method: str, payload: dict[str, Any]) -> Any:
    request_start = now_ts()
    request_id = uuid.uuid4().hex
    if method == "code.search" and payload.get("extension_guid") in {None, ""}:
        payload = dict(payload)
        payload.pop("extension_guid", None)
    request_payload = apply_freshness_request_policy(payload, method)
    configuration_view_error = request_payload.pop("_configuration_view_error", None)
    if configuration_view_error:
        return {
            "schema": "adapter_1c_mcp_policy.v1",
            "status": "invalid_argument",
            "method": method,
            "error": "invalid_configuration_view",
            "diagnostics": {"message": str(configuration_view_error)},
        }
    request_payload["_mcp_request_id"] = request_id
    payload = request_payload
    runtime_guardrail = runtime_form_inspection_unsupported(method, payload)
    if runtime_guardrail is not None:
        return enrich_result_with_freshness(payload, method, runtime_guardrail, request_start)
    technical_field_guardrail = normal_agent_technical_field_guardrail(method, payload)
    if technical_field_guardrail is not None:
        return enrich_result_with_freshness(payload, method, technical_field_guardrail, request_start)
    code_guardrail = metadata_write_code_guardrail(method, payload)
    if code_guardrail is not None:
        return enrich_result_with_freshness(payload, method, code_guardrail, request_start)
    if method_requires_base_id(method) and not str(payload.get("base_id") or "").strip():
        return missing_base_id_policy(method)
    if (method.startswith(DIAGNOSTIC_METHOD_PREFIXES) or method in DIAGNOSTIC_METHODS) and not diagnostic_mode_authorized(payload):
        return {
            "schema": "adapter_1c_mcp_policy.v1",
            "status": "blocked",
            "method": method,
            "reason": "diagnostic_method",
            "diagnostics": {
                "message": (
                    "This is a low-level diagnostic method and must not be used as a fallback for user-facing metadata answers. "
                    "Use metadata.object.attributes, metadata.object.full, metadata.object.forms, metadata.form.decode, "
                    "metadata.resolve_overrides, code.search, code.read, modules.search, metadata.definition.find, templates.bindings, "
                    "or modules.read. "
                    "Developer diagnostics require diagnostic=true and ONEC_MCP_ALLOW_DIAGNOSTIC=true in the MCP deployment."
                )
            },
        }
    force_async = truthy(payload.get("_mcp_async")) or truthy(payload.get("mcp_async"))
    force_sync = truthy(payload.get("_mcp_sync")) or truthy(payload.get("mcp_sync"))
    special_details_long = method in {"metadata.object.properties", "metadata.object.special.details"} and str(payload.get("kind") or "").strip().lower() in {
        "documentjournal",
        "журналдокументов",
        "журнал документов",
    }
    if method == "modules.search":
        payload = dict(payload)
        if "resolve_owners" not in payload:
            payload["resolve_owners"] = True
    if method == "metadata.object.full":
        sections_error = validate_metadata_object_full_sections(payload)
        if sections_error is not None:
            return sections_error
    if method in UNIFIED_METHODS:
        try:
            result = _run_unified_method(method, payload, request_start, request_id)
        except AdapterError as exc:
            return enrich_result_with_freshness(payload, method, adapter_error_result(method, exc), request_start)
        except Exception as exc:
            return enrich_result_with_freshness(
                payload,
                method,
                public_error(method, "mcp_request_exception", {"message": str(exc), "traceback": traceback.format_exc(limit=8)}),
                request_start,
            )
        return enrich_result_with_freshness(payload, method, result, request_start)

    # non-unified legacy methods keep previous behavior:
    scan_limit = 0
    raw_scan_limit = payload.get("scan_limit")
    if raw_scan_limit is not None:
        try:
            scan_limit = int(raw_scan_limit)
        except (TypeError, ValueError):
            scan_limit = 0
    if scan_limit < 0:
        scan_limit = 0

    should_enqueue = force_async or ((method in LONG_METHODS or special_details_long) and not force_sync)
    if method in {"modules.search", "code.search"} and scan_limit and scan_limit > 1500 and not force_sync:
        should_enqueue = True
    if method == "code.read":
        raw_text_limit = payload.get("max_chars")
        if raw_text_limit in {None, ""}:
            raw_text_limit = payload.get("read_max_chars")
        read_limit = None
        try:
            read_limit = int(raw_text_limit) if raw_text_limit is not None else None
        except (TypeError, ValueError):
            read_limit = None
        if read_limit is None:
            read_limit = 100000
        if read_limit >= HEAVY_TEXT_THRESHOLD_FOR_CODE_READ and not force_sync:
            should_enqueue = True
    if not should_enqueue:
        try:
            result = call_adapter_method(method, payload)
        except AdapterError as exc:
            return enrich_result_with_freshness(payload, method, adapter_error_result(method, exc), request_start)
        except Exception as exc:
            return enrich_result_with_freshness(
                payload,
                method,
                public_error(method, "mcp_request_exception", {"message": str(exc), "traceback": traceback.format_exc(limit=8)}),
                request_start,
            )
        if method == "modules.read":
            result = enrich_modules_read_result(payload, result)
        if method in {"modules.search", "code.search", "metadata.object.full", "metadata.objects.list"}:
            result = enrich_owner_metadata_in_data(payload, result)
        if method == "metadata.object.full":
            result = _enrich_metadata_object_full_display(payload, result)
        elif method == "metadata.objects.list":
            result = _enrich_metadata_objects_list_display(payload, result)
        elif method == "modules.search":
            result = _enrich_modules_search_display(payload, result)
        return enrich_result_with_freshness(payload, method, result, request_start)
    clean_payload = {key: value for key, value in payload.items() if key not in {"_mcp_async", "mcp_async", "_mcp_sync", "mcp_sync"}}
    try:
        accepted = call_adapter_method("adapter.job.start", {"method": method, "payload": clean_payload}, timeout=adapter_timeout())
    except AdapterError as exc:
        return enrich_result_with_freshness(payload, "adapter.job.start", adapter_error_result("adapter.job.start", exc), request_start)
    except Exception as exc:
        return enrich_result_with_freshness(
            payload,
            "adapter.job.start",
            public_error("adapter.job.start", "mcp_request_exception", {"message": str(exc), "traceback": traceback.format_exc(limit=8)}),
            request_start,
        )
    if not isinstance(accepted, dict) or accepted.get("status") not in {"accepted", "done", "error"}:
        return enrich_result_with_freshness(payload, method, accepted, request_start)
    job_id = str(accepted.get("job_id") or "")
    if not job_id:
        return enrich_result_with_freshness(payload, method, accepted, request_start)
    deadline = time.time() + job_fast_wait_seconds()
    while time.time() < deadline:
        try:
            job = call_adapter_method("adapter.job.get", {"job_id": job_id}, timeout=adapter_timeout())
        except AdapterError as exc:
            return enrich_result_with_freshness(payload, "adapter.job.get", adapter_error_result("adapter.job.get", exc), request_start)
        if job.get("status") in {"done", "error", "timeout", "cancelled"}:
            if job.get("status") == "done":
                return enrich_result_with_freshness(payload, method, maybe_enrich_owner_fields(method, payload, job.get("result")), request_start)
            return enrich_result_with_freshness(payload, method, job, request_start)
        time.sleep(0.05)
    try:
        job = call_adapter_method("adapter.job.get", {"job_id": job_id}, timeout=adapter_timeout())
    except AdapterError:
        job = accepted
    return enrich_result_with_freshness(
        payload,
        method,
        {
        "schema": "adapter_1c_mcp_job.v1",
        "status": "accepted",
        "job_id": job_id,
        "method": method,
        "source": "adapter",
        "timeout_seconds": job.get("timeout_seconds"),
        "progress": job.get("progress"),
        "current_step": job.get("current_step"),
        "poll": {"tool": "onec_request", "method": "mcp.job.get", "payload": {"job_id": job_id}},
        "cancel": {"tool": "onec_request", "method": "mcp.job.cancel", "payload": {"job_id": job_id}},
        "diagnostics": {
            "message": "Long adapter request is running in the MCP proxy. Poll mcp.job.get with this job_id instead of falling back to SQL diagnostics.",
        },
        },
        request_start,
    )


def tool_text(data: Any) -> dict[str, Any]:
    return {
        "content": [
            {
                "type": "text",
                "text": json.dumps(data, ensure_ascii=False, indent=2),
            }
        ]
    }


def access_audit_slugify(value: str, *, max_length: int = 80) -> str:
    slug = re.sub(r"[^0-9A-Za-zА-Яа-яЁё._-]+", "-", value.strip())
    slug = re.sub(r"-+", "-", slug).strip("-._")
    return (slug or "role-audit")[:max_length]


def access_audit_read_json(path: Path) -> dict[str, Any]:
    data = json.loads(path.read_text(encoding="utf-8-sig"))
    if not isinstance(data, dict):
        raise ValueError(f"JSON root is not an object: {path}")
    return data


def access_audit_summary_role(summary: dict[str, Any]) -> str | None:
    query = summary.get("query") if isinstance(summary.get("query"), dict) else {}
    role = query.get("role") if isinstance(query, dict) else None
    return str(role) if role is not None else None


def access_audit_find_latest_summaries(report_root: Path, base_id: str, *, role: str, count: int = 2) -> list[Path]:
    folder = report_root / access_audit_slugify(base_id, max_length=60)
    summaries: list[tuple[str, Path]] = []
    role_filter = role.casefold()
    for path in folder.glob("*.summary.json"):
        try:
            summary = access_audit_read_json(path)
        except (OSError, json.JSONDecodeError, ValueError):
            continue
        if (access_audit_summary_role(summary) or "").casefold() != role_filter:
            continue
        summaries.append((str(summary.get("generated_at") or ""), path))
    summaries.sort(key=lambda item: item[0], reverse=True)
    return [path for _, path in summaries[:count]]


def access_audit_load_export(path: Path) -> dict[str, Any]:
    data = access_audit_read_json(path)
    artifacts = data.get("artifacts") if isinstance(data.get("artifacts"), dict) else {}
    export_path = artifacts.get("json")
    if export_path:
        return access_audit_read_json(Path(str(export_path)))
    return data


def access_audit_user_key(row: dict[str, Any]) -> str:
    return str(row.get("user_id") or row.get("user_name") or "").strip()


def access_audit_group_rows_by_user(export: dict[str, Any]) -> dict[str, dict[str, Any]]:
    users: dict[str, dict[str, Any]] = {}
    rows = export.get("rows") if isinstance(export.get("rows"), list) else []
    for row in rows:
        if not isinstance(row, dict):
            continue
        key = access_audit_user_key(row)
        if not key:
            continue
        item = users.setdefault(
            key,
            {
                "user": {
                    "user_id": row.get("user_id"),
                    "user_name": row.get("user_name"),
                    "user_type": row.get("user_type"),
                    "user_active": row.get("user_active"),
                    "user_marked": row.get("user_marked"),
                },
                "access_paths": set(),
            },
        )
        if row.get("access_path"):
            item["access_paths"].add(str(row.get("access_path")))
    for item in users.values():
        item["access_paths"] = sorted(item["access_paths"])
    return users


def access_audit_compare_exports(old_export: dict[str, Any], new_export: dict[str, Any]) -> dict[str, Any]:
    old_users = access_audit_group_rows_by_user(old_export)
    new_users = access_audit_group_rows_by_user(new_export)
    old_keys = set(old_users)
    new_keys = set(new_users)
    added_keys = sorted(new_keys - old_keys)
    removed_keys = sorted(old_keys - new_keys)
    common_keys = sorted(old_keys & new_keys)
    changed_paths = [
        {
            "user": new_users[key]["user"],
            "old_access_paths": old_users[key]["access_paths"],
            "new_access_paths": new_users[key]["access_paths"],
        }
        for key in common_keys
        if old_users[key]["access_paths"] != new_users[key]["access_paths"]
    ]
    return {
        "schema": "onec_access_role_audit_compare.v1",
        "status": "ok",
        "counts": {
            "old_users": len(old_keys),
            "new_users": len(new_keys),
            "added_users": len(added_keys),
            "removed_users": len(removed_keys),
            "unchanged_users": len(common_keys),
            "changed_access_paths": len(changed_paths),
        },
        "added_users": [new_users[key]["user"] for key in added_keys],
        "removed_users": [old_users[key]["user"] for key in removed_keys],
        "changed_access_paths": changed_paths,
    }


def access_audit_compare_files(old_path: Path, new_path: Path, *, output: Path | None = None, html_output: Path | None = None) -> dict[str, Any]:
    result = access_audit_compare_exports(access_audit_load_export(old_path), access_audit_load_export(new_path))
    result["sources"] = {"old": str(old_path), "new": str(new_path)}
    result["artifacts"] = {
        **({"json": str(output)} if output is not None else {}),
        **({"html": str(html_output)} if html_output is not None else {}),
    }
    if output is not None:
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
    if html_output is not None:
        html_output.parent.mkdir(parents=True, exist_ok=True)
        html_output.write_text("<!doctype html><html><body><pre>" + json.dumps(result, ensure_ascii=False, indent=2) + "</pre></body></html>", encoding="utf-8")
    return result


def access_role_audit_compare_latest(args: dict[str, Any]) -> dict[str, Any]:
    base_id = str(args.get("base_id") or "").strip()
    role = str(args.get("role") or "").strip()
    if not base_id:
        return missing_base_id_policy("access.role.audit_compare_latest")
    if not role:
        return public_error("access.role.audit_compare_latest", "role_required", {"message": "role is required"})
    report_root = Path(str(args.get("report_root") or os.environ.get("ONEC_ACCESS_REPORT_ROOT") or DEFAULT_ACCESS_REPORT_ROOT))
    latest = access_audit_find_latest_summaries(report_root, base_id, role=role, count=2)
    if len(latest) < 2:
        return {
            "schema": "onec_access_role_audit_compare_latest.v1",
            "status": "not_enough_reports",
            "base_id": base_id,
            "role": role,
            "reports_found": len(latest),
            "message": "Need at least two saved audit summaries for this base and role.",
        }
    new_path, old_path = latest[0], latest[1]
    output = None
    html_output = None
    if truthy(args.get("write_artifacts", True)):
        folder = report_root / access_audit_slugify(base_id, max_length=60)
        stem = f"compare-{old_path.stem.replace('.summary', '')}-{new_path.stem.replace('.summary', '')}"
        output = folder / f"{stem}.json"
        html_output = folder / f"{stem}.html"
    result = access_audit_compare_files(old_path, new_path, output=output, html_output=html_output)
    return {"schema": "onec_access_role_audit_compare_latest.v1", **result, "base_id": base_id, "role": role}


def handle_tool_call(name: str, arguments: dict[str, Any] | None) -> dict[str, Any]:
    args = arguments or {}
    if name == "onec_health":
        result: dict[str, Any] = {
            "schema": "adapter_1c_mcp_health.v1",
            "mcp": {"status": "ok", "adapter_url": adapter_url()},
            "adapter": None,
        }
        try:
            result["adapter"] = call_adapter_method("health", {"base_id": args.get("base_id")} if args.get("base_id") else {})
        except AdapterError as exc:
            result["adapter"] = adapter_error_result("health", exc)
        return tool_text(result)
    if name == "onec_help":
        payload = {"method": args.get("method")} if args.get("method") else {}
        try:
            return tool_text(call_adapter_method("help.methods", payload))
        except AdapterError as exc:
            return tool_text(
                {
                    "schema": "adapter_1c_mcp_methods.v1",
                    "source": "adapter_unavailable",
                    "adapter_error": str(exc),
                    "methods": [],
                }
            )
    if name == "onec_request":
        method = str(args.get("method") or "").strip()
        if not method:
            return tool_text(public_error("onec_request", "method_required", {"message": "method is required"}))
        payload = args.get("payload") or {}
        if not isinstance(payload, dict):
            return tool_text(public_error(method or "onec_request", "invalid_payload", {"message": "payload must be an object"}))
        payload, selector_error = resolve_selector_token(method, payload)
        if selector_error is not None:
            return tool_text(selector_error)
        assert payload is not None
        if method in {"mcp.job.get", "adapter.job.get", "onec.job.get"}:
            job_id = str(payload.get("job_id") or "").strip()
            if not job_id:
                return tool_text(public_error(method, "job_id_required", {"message": "payload.job_id is required"}))
            try:
                job = call_adapter_method("adapter.job.get", {"job_id": job_id, "consume": truthy(payload.get("consume"))})
                if (
                    isinstance(job, dict)
                    and job.get("status") == "done"
                    and isinstance(job.get("result"), (dict, list))
                ):
                    job = dict(job)
                    job["result"] = maybe_enrich_owner_fields(str(job.get("method") or "").strip(), job.get("payload"), job.get("result"))
                return tool_text(publicize_read_selectors(job))
            except AdapterError as exc:
                return tool_text(adapter_error_result("adapter.job.get", exc))
        if method in {"mcp.job.cancel", "adapter.job.cancel", "onec.job.cancel"}:
            job_id = str(payload.get("job_id") or "").strip()
            if not job_id:
                return tool_text(public_error(method, "job_id_required", {"message": "payload.job_id is required"}))
            try:
                return tool_text(call_adapter_method("adapter.job.cancel", {"job_id": job_id}))
            except AdapterError as exc:
                return tool_text(adapter_error_result("adapter.job.cancel", exc))
        return tool_text(publicize_read_selectors(run_or_enqueue_adapter_method(method, payload)))
    if name == "onec_job_get":
        job_id = str(args.get("job_id") or "").strip()
        if not job_id:
            return tool_text(public_error("onec_job_get", "job_id_required", {"message": "job_id is required"}))
        try:
            job = call_adapter_method("adapter.job.get", {"job_id": job_id, "consume": truthy(args.get("consume"))})
            if (
                isinstance(job, dict)
                and job.get("status") == "done"
                and isinstance(job.get("result"), (dict, list))
            ):
                job = dict(job)
                job["result"] = maybe_enrich_owner_fields(str(job.get("method") or "").strip(), job.get("payload"), job.get("result"))
            return tool_text(publicize_read_selectors(job))
        except AdapterError as exc:
            return tool_text(adapter_error_result("adapter.job.get", exc))
    if name == "onec_job_cancel":
        job_id = str(args.get("job_id") or "").strip()
        if not job_id:
            return tool_text(public_error("onec_job_cancel", "job_id_required", {"message": "job_id is required"}))
        try:
            return tool_text(call_adapter_method("adapter.job.cancel", {"job_id": job_id}))
        except AdapterError as exc:
            return tool_text(adapter_error_result("adapter.job.cancel", exc))
    if name == "access_role_audit_compare_latest":
        return tool_text(access_role_audit_compare_latest(args))
    access_tool_methods = {
        "infobase_users_search": "infobase.users.search",
        "infobase_user_get": "infobase.user.get",
        "infobase_user_password_capabilities": "infobase.user.password.capabilities",
        "infobase_user_password_status": "infobase.user.password.status",
        "infobase_user_password_set": "infobase.user.password.set",
        "infobase_user_password_clear": "infobase.user.password.clear",
        "access_role_users": "access.role.users",
        "access_role_profiles": "access.role.profiles",
        "access_role_audit_export": "access.role.audit_export",
        "access_role_audit_analyze": "access.role.audit_analyze",
        "access_user_explain": "access.user.explain",
        "access_users_search": "access.users.search",
        "access_object_explain": "access.object.explain",
        "access_keys_query": "access.keys.query",
        "access_object_keys_resolve": "access.object_keys.resolve",
        "access_object_roles": "access.object.roles",
        "access_object_subjects": "access.object.subjects",
        "access_rls_discover": "access.rls.discover",
    }
    if name in access_tool_methods:
        method = access_tool_methods[name]
        if not str(args.get("base_id") or "").strip():
            return tool_text(missing_base_id_policy(method))
        try:
            return tool_text(call_adapter_method(method, dict(args), timeout=adapter_timeout()))
        except AdapterError as exc:
            return tool_text(adapter_error_result(method, exc))
    return tool_text(public_error(name, "unknown_tool", {"message": f"Unknown tool `{name}`"}))


def jsonrpc_error(request_id: Any, code: int, message: str, data: Any | None = None) -> dict[str, Any]:
    error: dict[str, Any] = {"code": code, "message": message}
    if data is not None:
        error["data"] = data
    return {"jsonrpc": "2.0", "id": request_id, "error": error}


def jsonrpc_result(request_id: Any, result: Any) -> dict[str, Any]:
    return {"jsonrpc": "2.0", "id": request_id, "result": result}


def handle_jsonrpc(payload: dict[str, Any]) -> dict[str, Any] | None:
    request_id = payload.get("id")
    method = payload.get("method")
    params = payload.get("params") or {}
    try:
        if method == "initialize":
            return jsonrpc_result(
                request_id,
                {
                    "protocolVersion": PROTOCOL_VERSION,
                    "capabilities": {"tools": {"listChanged": False}},
                    "serverInfo": {"name": "adapter-1c-mcp", "version": "0.1.0", "contract_version": MCP_CONTRACT_VERSION},
                },
            )
        if method == "notifications/initialized":
            return None
        if method == "ping":
            return jsonrpc_result(request_id, {})
        if method == "tools/list":
            return jsonrpc_result(request_id, {"contract_version": MCP_CONTRACT_VERSION, "tools": TOOLS})
        if method == "tools/call":
            return jsonrpc_result(request_id, handle_tool_call(str(params.get("name") or ""), params.get("arguments") or {}))
        return jsonrpc_error(request_id, -32601, f"Method not found: {method}")
    except Exception as exc:
        data: dict[str, Any] = {"message": str(exc)}
        if truthy(os.environ.get("ONEC_MCP_DEBUG_DIAGNOSTICS")):
            data["traceback"] = traceback.format_exc()
        return jsonrpc_error(request_id, -32000, "MCP request failed", data)


def payload_has_method(payload: Any, method: str) -> bool:
    if isinstance(payload, dict):
        return payload.get("method") == method
    if isinstance(payload, list):
        return any(isinstance(item, dict) and item.get("method") == method for item in payload)
    return False


def handle_jsonrpc_payload(payload: Any) -> dict[str, Any] | list[dict[str, Any]] | None:
    if isinstance(payload, list):
        responses = [response for item in payload if isinstance(item, dict) for response in [handle_jsonrpc(item)] if response is not None]
        return responses or None
    if isinstance(payload, dict):
        return handle_jsonrpc(payload)
    return jsonrpc_error(None, -32600, "Invalid JSON-RPC payload")


def sse_event(event: str, data: str) -> bytes:
    lines = [f"event: {event}", *(f"data: {line}" for line in data.splitlines() or [""]), "", ""]
    return ("\n".join(lines)).encode("utf-8")


class McpHandler(BaseHTTPRequestHandler):
    server_version = "adapter-1c-mcp/0.1"

    def log_message(self, fmt: str, *args: Any) -> None:
        print(f"{self.address_string()} - {fmt % args}", file=sys.stderr, flush=True)

    def write_json(self, status: int, data: Any, extra_headers: dict[str, str] | None = None) -> None:
        encoded = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(encoded)))
        self.send_header("Access-Control-Allow-Origin", "*")
        for key, value in (extra_headers or {}).items():
            self.send_header(key, value)
        self.end_headers()
        self.wfile.write(encoded)

    def write_no_content(self, status: int = 202, extra_headers: dict[str, str] | None = None) -> None:
        self.send_response(status)
        self.send_header("Access-Control-Allow-Origin", "*")
        for key, value in (extra_headers or {}).items():
            self.send_header(key, value)
        self.end_headers()

    def read_json(self) -> Any:
        length = int(self.headers.get("Content-Length") or 0)
        raw = self.rfile.read(length).decode("utf-8") if length else "{}"
        return json.loads(raw) if raw.strip() else {}

    def do_OPTIONS(self) -> None:
        self.send_response(204)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization, Mcp-Session-Id")
        self.end_headers()

    def do_GET(self) -> None:
        parsed = urllib.parse.urlparse(self.path)
        if parsed.path == "/health":
            self.write_json(200, {"status": "ok", "name": "adapter-1c-mcp", "contract_version": MCP_CONTRACT_VERSION, "adapter_url": adapter_url()})
            return
        if parsed.path == "/tools":
            self.write_json(200, {"contract_version": MCP_CONTRACT_VERSION, "tools": TOOLS})
            return
        if parsed.path not in {"/sse", "/mcp"}:
            self.write_json(404, {"error": "not found"})
            return

        session_id = uuid.uuid4().hex
        events: "queue.Queue[dict[str, Any] | None]" = queue.Queue()
        with SESSION_LOCK:
            SESSIONS[session_id] = events

        self.send_response(200)
        self.send_header("Content-Type", "text/event-stream; charset=utf-8")
        self.send_header("Cache-Control", "no-cache")
        self.send_header("Connection", "keep-alive")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()
        endpoint = f"/messages?session_id={session_id}"
        self.wfile.write(sse_event("endpoint", endpoint))
        self.wfile.flush()
        try:
            while True:
                try:
                    item = events.get(timeout=15)
                except queue.Empty:
                    self.wfile.write(b": keepalive\n\n")
                    self.wfile.flush()
                    continue
                if item is None:
                    break
                self.wfile.write(sse_event("message", json.dumps(item, ensure_ascii=False)))
                self.wfile.flush()
        except (BrokenPipeError, ConnectionResetError):
            pass
        finally:
            with SESSION_LOCK:
                SESSIONS.pop(session_id, None)

    def do_POST(self) -> None:
        parsed = urllib.parse.urlparse(self.path)
        if parsed.path in {"/mcp", "/"}:
            payload = self.read_json()
            response = handle_jsonrpc_payload(payload)
            headers: dict[str, str] = {}
            incoming_session_id = self.headers.get("Mcp-Session-Id", "").strip()
            if incoming_session_id:
                headers["Mcp-Session-Id"] = incoming_session_id
            if payload_has_method(payload, "initialize"):
                headers["Mcp-Session-Id"] = uuid.uuid4().hex
            if response is None:
                self.write_no_content(202, headers)
                return
            self.write_json(200, response or {}, headers)
            return
        if parsed.path != "/messages":
            self.write_json(404, {"error": "not found"})
            return
        query_params = urllib.parse.parse_qs(parsed.query)
        session_id = (query_params.get("session_id") or [""])[0]
        with SESSION_LOCK:
            events = SESSIONS.get(session_id)
        if events is None:
            self.write_json(404, {"error": "unknown session"})
            return
        payload = self.read_json()
        response = handle_jsonrpc_payload(payload)
        if response is not None:
            events.put(response)
        self.write_json(202, {"status": "accepted"})

    def do_DELETE(self) -> None:
        parsed = urllib.parse.urlparse(self.path)
        if parsed.path not in {"/mcp", "/sse", "/messages"}:
            self.write_json(404, {"error": "not found"})
            return
        session_id = self.headers.get("Mcp-Session-Id", "").strip()
        if not session_id:
            query_params = urllib.parse.parse_qs(parsed.query)
            session_id = (query_params.get("session_id") or [""])[0]
        if session_id:
            with SESSION_LOCK:
                events = SESSIONS.pop(session_id, None)
            if events is not None:
                events.put(None)
        self.write_no_content(202)


def main() -> int:
    parser = argparse.ArgumentParser(description="Run adapter-1c MCP proxy.")
    parser.add_argument("--host", default=os.environ.get("HOST", "0.0.0.0"))
    parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", "8021")))
    args = parser.parse_args()
    server = ThreadingHTTPServer((args.host, args.port), McpHandler)
    print(f"adapter-1c-mcp listening on http://{args.host}:{args.port}", flush=True)
    print(f"ONEC_ADAPTER_URL={adapter_url()}", flush=True)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        with SESSION_LOCK:
            for events in SESSIONS.values():
                events.put(None)
        server.server_close()
        time.sleep(0.1)
    return 0


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