😏
This commit is contained in:
@@ -0,0 +1,524 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user