525 lines
21 KiB
Python
525 lines
21 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Operator-owned tool MCP registry, validation, and Codex rendering.
|
||
|
|
|
||
|
|
Tool MCP servers expose third-party tools to Codex. They are deliberately
|
||
|
|
separate from the MMO Agent MCP server (``mmo_mesh``), which launches and
|
||
|
|
supervises profile participants.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import math
|
||
|
|
import os
|
||
|
|
import re
|
||
|
|
import shutil
|
||
|
|
from collections.abc import Mapping
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any
|
||
|
|
from urllib.parse import urlsplit
|
||
|
|
|
||
|
|
from mmo_util import (
|
||
|
|
config_root,
|
||
|
|
parse_env_file,
|
||
|
|
read_toml,
|
||
|
|
valid_absolute_uri,
|
||
|
|
valid_http_header_value,
|
||
|
|
validate_id,
|
||
|
|
)
|
||
|
|
from mmo_version import MMO_SCHEMA_VERSION
|
||
|
|
|
||
|
|
TOOL_MCP_RESERVED_IDS = {"mmo_mesh"}
|
||
|
|
TOOL_MCP_TRANSPORTS = {"stdio", "streamable_http"}
|
||
|
|
TOOL_MCP_APPROVAL_MODES = {"auto", "prompt", "writes", "approve"}
|
||
|
|
|
||
|
|
_DOCUMENT_FIELDS = {"schema_version", "tool_mcp_servers"}
|
||
|
|
_COMMON_SERVER_FIELDS = {
|
||
|
|
"transport",
|
||
|
|
"enabled_tools",
|
||
|
|
"default_tools_approval_mode",
|
||
|
|
"startup_timeout_sec",
|
||
|
|
"tool_timeout_sec",
|
||
|
|
"supports_parallel_tool_calls",
|
||
|
|
"tools",
|
||
|
|
}
|
||
|
|
_STDIO_FIELDS = {"command", "args", "env", "env_vars", "cwd"}
|
||
|
|
_HTTP_FIELDS = {
|
||
|
|
"url",
|
||
|
|
"bearer_token_env_var",
|
||
|
|
"http_headers",
|
||
|
|
"env_http_headers",
|
||
|
|
}
|
||
|
|
_OAUTH_FIELDS = {"auth", "scopes", "oauth", "oauth_resource", "environment_id"}
|
||
|
|
_REGISTRY_OWNED_POLICY_FIELDS = {"enabled", "required", "disabled_tools"}
|
||
|
|
_TOOL_FIELDS = {"approval_mode"}
|
||
|
|
|
||
|
|
_ENVIRONMENT_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
||
|
|
_HEADER_NAME = re.compile(r"[!#$%&'*+.^_`|~0-9A-Za-z-]+")
|
||
|
|
_SENSITIVE_ENVIRONMENT_NAME = re.compile(
|
||
|
|
r"(?:^|_)(?:API_?KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|COOKIE|AUTH|PRIVATE)(?:_|$)",
|
||
|
|
re.IGNORECASE,
|
||
|
|
)
|
||
|
|
_SENSITIVE_HEADER_NAME = re.compile(
|
||
|
|
r"(?:^|-)(?:authorization|cookie|api-?key|key|token|secret|password|passwd|"
|
||
|
|
r"credential|auth|private)(?:-|$)",
|
||
|
|
re.IGNORECASE,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def tool_mcp_registry_root() -> Path:
|
||
|
|
"""Return the operator-owned directory containing registry fragments."""
|
||
|
|
|
||
|
|
return config_root() / "tool-mcp.d"
|
||
|
|
|
||
|
|
|
||
|
|
def _reject_unknown_fields(value: Mapping[str, Any], allowed: set[str], label: str) -> None:
|
||
|
|
unknown = sorted(set(value) - allowed)
|
||
|
|
if unknown:
|
||
|
|
raise ValueError(f"{label} has unknown fields: {', '.join(unknown)}")
|
||
|
|
|
||
|
|
|
||
|
|
def _nonempty_string(value: Any, label: str) -> str:
|
||
|
|
if not isinstance(value, str) or not value.strip():
|
||
|
|
raise ValueError(f"{label} must be a non-empty string")
|
||
|
|
if any(ord(character) < 0x20 or ord(character) == 0x7F for character in value):
|
||
|
|
raise ValueError(f"{label} cannot contain control characters")
|
||
|
|
return value
|
||
|
|
|
||
|
|
|
||
|
|
def _string_list(value: Any, label: str, *, allow_empty: bool) -> list[str]:
|
||
|
|
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
||
|
|
raise ValueError(f"{label} must be an array of strings")
|
||
|
|
if not allow_empty and not value:
|
||
|
|
raise ValueError(f"{label} cannot be empty")
|
||
|
|
if any(not item.strip() for item in value):
|
||
|
|
raise ValueError(f"{label} cannot contain empty strings")
|
||
|
|
if len(value) != len(set(value)):
|
||
|
|
raise ValueError(f"{label} cannot contain duplicates")
|
||
|
|
return list(value)
|
||
|
|
|
||
|
|
|
||
|
|
def _environment_name(value: Any, label: str) -> str:
|
||
|
|
if not isinstance(value, str) or not _ENVIRONMENT_NAME.fullmatch(value):
|
||
|
|
raise ValueError(f"{label} must be a valid environment variable name")
|
||
|
|
return value
|
||
|
|
|
||
|
|
|
||
|
|
def _positive_seconds(value: Any, label: str) -> float:
|
||
|
|
if (
|
||
|
|
not isinstance(value, (int, float))
|
||
|
|
or isinstance(value, bool)
|
||
|
|
or not math.isfinite(float(value))
|
||
|
|
or not 0 < float(value) <= 172_800
|
||
|
|
):
|
||
|
|
raise ValueError(f"{label} must be a finite number between 0 and 172800")
|
||
|
|
return float(value)
|
||
|
|
|
||
|
|
|
||
|
|
def _string_map(value: Any, label: str) -> dict[str, str]:
|
||
|
|
if not isinstance(value, Mapping):
|
||
|
|
raise ValueError(f"{label} must be a table of strings")
|
||
|
|
result: dict[str, str] = {}
|
||
|
|
for raw_key, raw_value in value.items():
|
||
|
|
if not isinstance(raw_key, str) or not isinstance(raw_value, str):
|
||
|
|
raise ValueError(f"{label} must be a table of strings")
|
||
|
|
result[raw_key] = raw_value
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
def _validate_stdio_environment(value: Any, label: str) -> dict[str, str]:
|
||
|
|
environment = _string_map(value, label)
|
||
|
|
result: dict[str, str] = {}
|
||
|
|
for name, raw_value in environment.items():
|
||
|
|
_environment_name(name, f"{label} key")
|
||
|
|
if _SENSITIVE_ENVIRONMENT_NAME.search(name):
|
||
|
|
raise ValueError(
|
||
|
|
f"{label}.{name} looks credential-bearing; use env_vars and credentials.env"
|
||
|
|
)
|
||
|
|
if "\x00" in raw_value:
|
||
|
|
raise ValueError(f"{label}.{name} cannot contain NUL")
|
||
|
|
result[name] = raw_value
|
||
|
|
return dict(sorted(result.items()))
|
||
|
|
|
||
|
|
|
||
|
|
def _validate_headers(value: Any, label: str, *, environment_backed: bool) -> dict[str, str]:
|
||
|
|
headers = _string_map(value, label)
|
||
|
|
result: dict[str, str] = {}
|
||
|
|
seen: set[str] = set()
|
||
|
|
for raw_name, raw_value in headers.items():
|
||
|
|
if not _HEADER_NAME.fullmatch(raw_name):
|
||
|
|
raise ValueError(f"{label} contains an invalid HTTP header name: {raw_name!r}")
|
||
|
|
normalized = raw_name.lower()
|
||
|
|
if normalized in seen:
|
||
|
|
raise ValueError(f"{label} contains duplicate case-insensitive header {raw_name!r}")
|
||
|
|
seen.add(normalized)
|
||
|
|
if environment_backed:
|
||
|
|
_environment_name(raw_value, f"{label}.{raw_name}")
|
||
|
|
else:
|
||
|
|
if _SENSITIVE_HEADER_NAME.search(raw_name):
|
||
|
|
raise ValueError(
|
||
|
|
f"{label}.{raw_name} is credential-bearing; use bearer_token_env_var "
|
||
|
|
"or env_http_headers"
|
||
|
|
)
|
||
|
|
if not valid_http_header_value(raw_value):
|
||
|
|
raise ValueError(f"{label}.{raw_name} contains an invalid HTTP header value")
|
||
|
|
result[raw_name] = raw_value
|
||
|
|
return dict(sorted(result.items(), key=lambda item: item[0].lower()))
|
||
|
|
|
||
|
|
|
||
|
|
def _validate_http_url(value: Any, label: str) -> str:
|
||
|
|
url = _nonempty_string(value, label)
|
||
|
|
if not valid_absolute_uri(url):
|
||
|
|
raise ValueError(f"{label} must be a valid HTTP(S) URL")
|
||
|
|
try:
|
||
|
|
parsed = urlsplit(url)
|
||
|
|
_ = parsed.port
|
||
|
|
except ValueError as exc:
|
||
|
|
raise ValueError(f"{label} must be a valid HTTP(S) URL") from exc
|
||
|
|
if parsed.scheme.lower() not in {"http", "https"} or not parsed.hostname:
|
||
|
|
raise ValueError(f"{label} must be a valid HTTP(S) URL")
|
||
|
|
if parsed.username is not None or parsed.password is not None:
|
||
|
|
raise ValueError(f"{label} cannot contain embedded credentials")
|
||
|
|
if parsed.fragment:
|
||
|
|
raise ValueError(f"{label} cannot contain a URL fragment")
|
||
|
|
return url
|
||
|
|
|
||
|
|
|
||
|
|
def _validate_stdio_command(value: Any, label: str) -> str:
|
||
|
|
command = _nonempty_string(value, label)
|
||
|
|
if "/" not in command and not command.startswith("~"):
|
||
|
|
return command
|
||
|
|
try:
|
||
|
|
path = Path(command).expanduser()
|
||
|
|
except RuntimeError as exc:
|
||
|
|
raise ValueError(f"{label} has an unknown home-directory user") from exc
|
||
|
|
if not path.is_absolute():
|
||
|
|
raise ValueError(f"{label} must be a PATH executable name or an absolute path")
|
||
|
|
return str(path.resolve())
|
||
|
|
|
||
|
|
|
||
|
|
def _validate_tool_policy(
|
||
|
|
value: Any,
|
||
|
|
*,
|
||
|
|
label: str,
|
||
|
|
enabled_tools: set[str],
|
||
|
|
) -> dict[str, dict[str, str]]:
|
||
|
|
if value is None:
|
||
|
|
return {}
|
||
|
|
if not isinstance(value, Mapping):
|
||
|
|
raise ValueError(f"{label} must be a table")
|
||
|
|
result: dict[str, dict[str, str]] = {}
|
||
|
|
for raw_name, raw_policy in value.items():
|
||
|
|
tool_name = _nonempty_string(raw_name, f"{label} tool name")
|
||
|
|
if tool_name not in enabled_tools:
|
||
|
|
raise ValueError(f"{label}.{tool_name} is not present in enabled_tools")
|
||
|
|
if not isinstance(raw_policy, Mapping):
|
||
|
|
raise ValueError(f"{label}.{tool_name} must be a table")
|
||
|
|
_reject_unknown_fields(raw_policy, _TOOL_FIELDS, f"{label}.{tool_name}")
|
||
|
|
approval = raw_policy.get("approval_mode")
|
||
|
|
if not isinstance(approval, str) or approval not in TOOL_MCP_APPROVAL_MODES:
|
||
|
|
raise ValueError(
|
||
|
|
f"{label}.{tool_name}.approval_mode must be one of "
|
||
|
|
f"{sorted(TOOL_MCP_APPROVAL_MODES)}"
|
||
|
|
)
|
||
|
|
result[tool_name] = {"approval_mode": approval}
|
||
|
|
return dict(sorted(result.items()))
|
||
|
|
|
||
|
|
|
||
|
|
def validate_tool_mcp_server(server_id: str, value: Any, *, label: str) -> dict[str, Any]:
|
||
|
|
"""Validate and normalize one operator-defined tool MCP server."""
|
||
|
|
|
||
|
|
server_id = validate_id(server_id, "tool MCP server id")
|
||
|
|
if server_id in TOOL_MCP_RESERVED_IDS:
|
||
|
|
raise ValueError(f"tool MCP server id {server_id!r} is reserved for Agent MCP")
|
||
|
|
if not isinstance(value, Mapping):
|
||
|
|
raise ValueError(f"{label} must be a table")
|
||
|
|
if "bearer_token" in value:
|
||
|
|
raise ValueError(f"{label}.bearer_token is forbidden; use bearer_token_env_var")
|
||
|
|
oauth_fields = sorted(set(value) & _OAUTH_FIELDS)
|
||
|
|
if oauth_fields:
|
||
|
|
raise ValueError(
|
||
|
|
f"{label} uses unsupported OAuth fields: {', '.join(oauth_fields)}; "
|
||
|
|
"the Tool MCP registry supports environment-backed authentication only"
|
||
|
|
)
|
||
|
|
policy_fields = sorted(set(value) & _REGISTRY_OWNED_POLICY_FIELDS)
|
||
|
|
if policy_fields:
|
||
|
|
raise ValueError(f"{label} cannot set agent-owned fields: {', '.join(policy_fields)}")
|
||
|
|
transport = value.get("transport")
|
||
|
|
if not isinstance(transport, str) or transport not in TOOL_MCP_TRANSPORTS:
|
||
|
|
raise ValueError(f"{label}.transport must be one of {sorted(TOOL_MCP_TRANSPORTS)}")
|
||
|
|
allowed = _COMMON_SERVER_FIELDS | (_STDIO_FIELDS if transport == "stdio" else _HTTP_FIELDS)
|
||
|
|
_reject_unknown_fields(value, allowed, label)
|
||
|
|
|
||
|
|
enabled_tools = _string_list(
|
||
|
|
value.get("enabled_tools"), f"{label}.enabled_tools", allow_empty=False
|
||
|
|
)
|
||
|
|
approval = value.get("default_tools_approval_mode")
|
||
|
|
if not isinstance(approval, str) or approval not in TOOL_MCP_APPROVAL_MODES:
|
||
|
|
raise ValueError(
|
||
|
|
f"{label}.default_tools_approval_mode must be one of {sorted(TOOL_MCP_APPROVAL_MODES)}"
|
||
|
|
)
|
||
|
|
normalized: dict[str, Any] = {
|
||
|
|
"transport": transport,
|
||
|
|
"enabled_tools": enabled_tools,
|
||
|
|
"default_tools_approval_mode": approval,
|
||
|
|
"supports_parallel_tool_calls": False,
|
||
|
|
}
|
||
|
|
if "supports_parallel_tool_calls" in value:
|
||
|
|
if not isinstance(value["supports_parallel_tool_calls"], bool):
|
||
|
|
raise ValueError(f"{label}.supports_parallel_tool_calls must be boolean")
|
||
|
|
normalized["supports_parallel_tool_calls"] = value["supports_parallel_tool_calls"]
|
||
|
|
for timeout_field in ("startup_timeout_sec", "tool_timeout_sec"):
|
||
|
|
if timeout_field in value:
|
||
|
|
normalized[timeout_field] = _positive_seconds(
|
||
|
|
value[timeout_field], f"{label}.{timeout_field}"
|
||
|
|
)
|
||
|
|
normalized["tools"] = _validate_tool_policy(
|
||
|
|
value.get("tools"),
|
||
|
|
label=f"{label}.tools",
|
||
|
|
enabled_tools=set(enabled_tools),
|
||
|
|
)
|
||
|
|
|
||
|
|
if transport == "stdio":
|
||
|
|
normalized["command"] = _validate_stdio_command(value.get("command"), f"{label}.command")
|
||
|
|
normalized["args"] = _string_list(value.get("args", []), f"{label}.args", allow_empty=True)
|
||
|
|
if any("\x00" in argument for argument in normalized["args"]):
|
||
|
|
raise ValueError(f"{label}.args cannot contain NUL")
|
||
|
|
normalized["env"] = _validate_stdio_environment(value.get("env", {}), f"{label}.env")
|
||
|
|
env_vars = _string_list(value.get("env_vars", []), f"{label}.env_vars", allow_empty=True)
|
||
|
|
for name in env_vars:
|
||
|
|
_environment_name(name, f"{label}.env_vars")
|
||
|
|
overlap = sorted(set(normalized["env"]) & set(env_vars))
|
||
|
|
if overlap:
|
||
|
|
raise ValueError(
|
||
|
|
f"{label} defines the same variables in env and env_vars: {', '.join(overlap)}"
|
||
|
|
)
|
||
|
|
normalized["env_vars"] = env_vars
|
||
|
|
if "cwd" in value:
|
||
|
|
raw_cwd = _nonempty_string(value["cwd"], f"{label}.cwd")
|
||
|
|
try:
|
||
|
|
cwd = Path(raw_cwd).expanduser()
|
||
|
|
except RuntimeError as exc:
|
||
|
|
raise ValueError(f"{label}.cwd has an unknown home-directory user") from exc
|
||
|
|
if not cwd.is_absolute():
|
||
|
|
raise ValueError(f"{label}.cwd must expand to an absolute path")
|
||
|
|
normalized["cwd"] = str(cwd.resolve())
|
||
|
|
else:
|
||
|
|
normalized["url"] = _validate_http_url(value.get("url"), f"{label}.url")
|
||
|
|
if "bearer_token_env_var" in value:
|
||
|
|
normalized["bearer_token_env_var"] = _environment_name(
|
||
|
|
value["bearer_token_env_var"], f"{label}.bearer_token_env_var"
|
||
|
|
)
|
||
|
|
normalized["http_headers"] = _validate_headers(
|
||
|
|
value.get("http_headers", {}),
|
||
|
|
f"{label}.http_headers",
|
||
|
|
environment_backed=False,
|
||
|
|
)
|
||
|
|
normalized["env_http_headers"] = _validate_headers(
|
||
|
|
value.get("env_http_headers", {}),
|
||
|
|
f"{label}.env_http_headers",
|
||
|
|
environment_backed=True,
|
||
|
|
)
|
||
|
|
static_names = {name.lower() for name in normalized["http_headers"]}
|
||
|
|
environment_names = {name.lower() for name in normalized["env_http_headers"]}
|
||
|
|
overlap = sorted(static_names & environment_names)
|
||
|
|
if overlap:
|
||
|
|
raise ValueError(
|
||
|
|
f"{label} defines headers in both http_headers and env_http_headers: "
|
||
|
|
+ ", ".join(overlap)
|
||
|
|
)
|
||
|
|
return normalized
|
||
|
|
|
||
|
|
|
||
|
|
def _load_tool_mcp_fragment(path: Path) -> dict[str, dict[str, Any]]:
|
||
|
|
data = read_toml(path)
|
||
|
|
_reject_unknown_fields(data, _DOCUMENT_FIELDS, f"tool MCP registry {path}")
|
||
|
|
schema = data.get("schema_version")
|
||
|
|
if not isinstance(schema, int) or isinstance(schema, bool) or schema != MMO_SCHEMA_VERSION:
|
||
|
|
raise ValueError(f"unsupported tool MCP registry schema_version in {path}")
|
||
|
|
raw_servers = data.get("tool_mcp_servers", {})
|
||
|
|
if not isinstance(raw_servers, Mapping):
|
||
|
|
raise ValueError(f"tool_mcp_servers must be a table in {path}")
|
||
|
|
return {
|
||
|
|
server_id: validate_tool_mcp_server(
|
||
|
|
server_id,
|
||
|
|
server,
|
||
|
|
label=f"tool MCP server {server_id} in {path}",
|
||
|
|
)
|
||
|
|
for server_id, server in raw_servers.items()
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def load_tool_mcp_registry_with_sources() -> tuple[dict[str, dict[str, Any]], dict[str, str]]:
|
||
|
|
"""Load deterministic operator fragments, replacing duplicate entries whole."""
|
||
|
|
|
||
|
|
registry: dict[str, dict[str, Any]] = {}
|
||
|
|
sources: dict[str, str] = {}
|
||
|
|
root = tool_mcp_registry_root()
|
||
|
|
if root.is_dir():
|
||
|
|
for path in sorted(root.glob("*.toml")):
|
||
|
|
for server_id, server in _load_tool_mcp_fragment(path).items():
|
||
|
|
registry[server_id] = server
|
||
|
|
sources[server_id] = str(path)
|
||
|
|
return dict(sorted(registry.items())), dict(sorted(sources.items()))
|
||
|
|
|
||
|
|
|
||
|
|
def load_tool_mcp_registry() -> dict[str, dict[str, Any]]:
|
||
|
|
return load_tool_mcp_registry_with_sources()[0]
|
||
|
|
|
||
|
|
|
||
|
|
def validate_tool_mcp_grants(
|
||
|
|
value: Any,
|
||
|
|
registry: Mapping[str, Mapping[str, Any]],
|
||
|
|
*,
|
||
|
|
label: str,
|
||
|
|
) -> dict[str, dict[str, Any]]:
|
||
|
|
"""Validate an agent's required/optional grants against operator maxima."""
|
||
|
|
|
||
|
|
if value is None:
|
||
|
|
return {}
|
||
|
|
if not isinstance(value, Mapping):
|
||
|
|
raise ValueError(f"{label} must be a table")
|
||
|
|
result: dict[str, dict[str, Any]] = {}
|
||
|
|
for raw_server_id, raw_grant in value.items():
|
||
|
|
server_id = validate_id(raw_server_id, "tool MCP server id")
|
||
|
|
if server_id not in registry:
|
||
|
|
raise ValueError(
|
||
|
|
f"{label}.{server_id} references an undefined operator tool MCP server; "
|
||
|
|
f"define it under {tool_mcp_registry_root()}"
|
||
|
|
)
|
||
|
|
if not isinstance(raw_grant, Mapping):
|
||
|
|
raise ValueError(f"{label}.{server_id} must be a table")
|
||
|
|
_reject_unknown_fields(
|
||
|
|
raw_grant,
|
||
|
|
{"required", "enabled_tools"},
|
||
|
|
f"{label}.{server_id}",
|
||
|
|
)
|
||
|
|
required = raw_grant.get("required", True)
|
||
|
|
if not isinstance(required, bool):
|
||
|
|
raise ValueError(f"{label}.{server_id}.required must be boolean")
|
||
|
|
maximum = list(registry[server_id]["enabled_tools"])
|
||
|
|
selected = (
|
||
|
|
maximum
|
||
|
|
if "enabled_tools" not in raw_grant
|
||
|
|
else _string_list(
|
||
|
|
raw_grant["enabled_tools"],
|
||
|
|
f"{label}.{server_id}.enabled_tools",
|
||
|
|
allow_empty=False,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
outside_maximum = sorted(set(selected) - set(maximum))
|
||
|
|
if outside_maximum:
|
||
|
|
raise ValueError(
|
||
|
|
f"{label}.{server_id}.enabled_tools exceeds the operator allowlist: "
|
||
|
|
+ ", ".join(outside_maximum)
|
||
|
|
)
|
||
|
|
result[server_id] = {"required": required, "enabled_tools": selected}
|
||
|
|
return dict(sorted(result.items()))
|
||
|
|
|
||
|
|
|
||
|
|
def tool_mcp_environment_names(server: Mapping[str, Any]) -> list[str]:
|
||
|
|
"""Return process environment names referenced by a normalized definition."""
|
||
|
|
|
||
|
|
names: set[str] = set()
|
||
|
|
if server["transport"] == "stdio":
|
||
|
|
names.update(str(name) for name in server.get("env_vars", []))
|
||
|
|
else:
|
||
|
|
names.update(tool_mcp_http_environment_names(server))
|
||
|
|
return sorted(names)
|
||
|
|
|
||
|
|
|
||
|
|
def tool_mcp_http_environment_names(server: Mapping[str, Any]) -> list[str]:
|
||
|
|
"""Return environment names whose values become HTTP header material."""
|
||
|
|
|
||
|
|
if server["transport"] != "streamable_http":
|
||
|
|
return []
|
||
|
|
names = {str(name) for name in server.get("env_http_headers", {}).values()}
|
||
|
|
bearer = server.get("bearer_token_env_var")
|
||
|
|
if bearer:
|
||
|
|
names.add(str(bearer))
|
||
|
|
return sorted(names)
|
||
|
|
|
||
|
|
|
||
|
|
def codex_tool_mcp_server_config(
|
||
|
|
server: Mapping[str, Any],
|
||
|
|
grant: Mapping[str, Any] | None,
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
"""Compile one normalized definition/grant to Codex's mcp_servers shape."""
|
||
|
|
|
||
|
|
transport = str(server["transport"])
|
||
|
|
fields = _STDIO_FIELDS if transport == "stdio" else _HTTP_FIELDS
|
||
|
|
result = {
|
||
|
|
key: server[key] for key in sorted(fields) if key in server and server[key] not in ({}, [])
|
||
|
|
}
|
||
|
|
maximum = list(server["enabled_tools"])
|
||
|
|
selected = set(grant["enabled_tools"] if grant is not None else ())
|
||
|
|
result.update(
|
||
|
|
{
|
||
|
|
"enabled": grant is not None,
|
||
|
|
"required": bool(grant["required"]) if grant is not None else False,
|
||
|
|
"supports_parallel_tool_calls": bool(server["supports_parallel_tool_calls"]),
|
||
|
|
"enabled_tools": maximum,
|
||
|
|
# Always emit this array so a native role can clear a narrower
|
||
|
|
# deny-list inherited from its parent config layer.
|
||
|
|
"disabled_tools": [tool for tool in maximum if tool not in selected],
|
||
|
|
"default_tools_approval_mode": server["default_tools_approval_mode"],
|
||
|
|
}
|
||
|
|
)
|
||
|
|
for timeout_field in ("startup_timeout_sec", "tool_timeout_sec"):
|
||
|
|
if timeout_field in server:
|
||
|
|
result[timeout_field] = server[timeout_field]
|
||
|
|
if server.get("tools"):
|
||
|
|
result["tools"] = server["tools"]
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
def tool_mcp_readiness(
|
||
|
|
server_id: str,
|
||
|
|
server: Mapping[str, Any],
|
||
|
|
*,
|
||
|
|
sources: Mapping[str, str] | None = None,
|
||
|
|
) -> dict[str, Any]:
|
||
|
|
"""Return non-launching command, cwd, and environment readiness."""
|
||
|
|
|
||
|
|
values = parse_env_file(config_root() / "credentials.env")
|
||
|
|
values.update({key: value for key, value in os.environ.items() if value})
|
||
|
|
http_environment = set(tool_mcp_http_environment_names(server))
|
||
|
|
environment: dict[str, bool] = {}
|
||
|
|
invalid_environment: list[str] = []
|
||
|
|
for name in tool_mcp_environment_names(server):
|
||
|
|
value = values.get(name)
|
||
|
|
valid = bool(value) and "\x00" not in str(value)
|
||
|
|
if valid and name in http_environment:
|
||
|
|
valid = valid_http_header_value(str(value))
|
||
|
|
environment[name] = valid
|
||
|
|
if value and not valid:
|
||
|
|
invalid_environment.append(name)
|
||
|
|
result: dict[str, Any] = {
|
||
|
|
"id": server_id,
|
||
|
|
"source": (sources or {}).get(server_id),
|
||
|
|
"transport": server["transport"],
|
||
|
|
"enabled_tools": list(server["enabled_tools"]),
|
||
|
|
"default_tools_approval_mode": server["default_tools_approval_mode"],
|
||
|
|
"environment": environment,
|
||
|
|
"invalid_environment": invalid_environment,
|
||
|
|
}
|
||
|
|
if server["transport"] == "stdio":
|
||
|
|
command = str(server["command"])
|
||
|
|
command_path = Path(command).expanduser()
|
||
|
|
resolved = (
|
||
|
|
str(command_path.resolve())
|
||
|
|
if "/" in command and command_path.is_file() and os.access(command_path, os.X_OK)
|
||
|
|
else shutil.which(command)
|
||
|
|
)
|
||
|
|
result["command"] = command
|
||
|
|
result["resolved_command"] = resolved
|
||
|
|
cwd = server.get("cwd")
|
||
|
|
result["cwd"] = cwd
|
||
|
|
result["cwd_ready"] = cwd is None or Path(str(cwd)).is_dir()
|
||
|
|
result["transport_ready"] = bool(resolved) and bool(result["cwd_ready"])
|
||
|
|
else:
|
||
|
|
result["url"] = server["url"]
|
||
|
|
result["transport_ready"] = True
|
||
|
|
result["environment_ready"] = all(environment.values())
|
||
|
|
result["ready"] = bool(result["transport_ready"]) and bool(result["environment_ready"])
|
||
|
|
return result
|