Files
Artemis-Eapil/artemis/interfaces/cli/commands/mcp.py
T

272 lines
9.6 KiB
Python

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Model Context Protocol (MCP) server command (artemis mcp)."""
import json
import os
from pathlib import Path
import re
import sys
import threading
import tomllib
from typing import Annotated
from mcp_server.utils import env_utils
from artemis.runtime import shutdown_awake_service, start_awake_service
from artemis.utils.logger import get_logger
from rich.console import Console
from rich.syntax import Syntax
import typer
logger = get_logger(__name__)
console = Console()
ARTEMIS_MCP_TOOLS = (
"mobile_run_task",
"mobile_manage_task",
"mobile_get_device_state",
"mobile_inspect_trace",
"mobile_diagnose",
"mobile_workspace",
"mobile_config",
"mobile_task_file",
"mobile_batch",
"mobile_favorites",
"mobile_exploration",
"mobile_search_history",
"mobile_export_report",
)
def _get_config_snippet(client: str, python_exe: str, project_root: str) -> dict:
"""Generate the supported Codex MCP configuration."""
if client != "codex":
raise ValueError("Only Codex MCP configuration is supported")
return {
"mcp_servers": {
"artemis": {
"command": python_exe,
"args": ["-m", "mcp_server"],
"cwd": project_root,
"env": {
"PYTHONUNBUFFERED": "1",
"PYTHONPATH": project_root,
"ARTEMIS_DESKTOP_NOTIFY": "true",
},
"enabled": True,
"required": False,
"startup_timeout_sec": 120,
"enabled_tools": list(ARTEMIS_MCP_TOOLS),
}
}
}
def _codex_toml_block(server_config: dict) -> str:
"""Renders a managed Codex MCP server block using TOML-compatible JSON strings."""
command = json.dumps(str(server_config["command"]), ensure_ascii=False)
args = json.dumps(server_config.get("args", []), ensure_ascii=False)
cwd = json.dumps(str(server_config["cwd"]), ensure_ascii=False)
env = server_config.get("env", {})
lines = [
"# BEGIN ARTEMIS MCP CONFIG",
"[mcp_servers.artemis]",
f"command = {command}",
f"args = {args}",
f"cwd = {cwd}",
]
for key in ("enabled", "required"):
if key in server_config:
lines.append(f"{key} = {'true' if server_config[key] else 'false'}")
if "startup_timeout_sec" in server_config:
lines.append(f"startup_timeout_sec = {int(server_config['startup_timeout_sec'])}")
if "enabled_tools" in server_config:
lines.append(
f"enabled_tools = {json.dumps(server_config['enabled_tools'], ensure_ascii=False)}"
)
if env:
lines.extend(["", "[mcp_servers.artemis.env]"])
for key, value in env.items():
lines.append(f"{key} = {json.dumps(str(value), ensure_ascii=False)}")
lines.append("# END ARTEMIS MCP CONFIG")
return "\n".join(lines) + "\n"
def _merge_codex_toml(file_path: Path, server_config: dict) -> bool:
from artemis.integrations.codex_mcp import refresh_codex
try:
result = refresh_codex(
file_path,
Path(server_config["cwd"]),
server_config["command"],
ARTEMIS_MCP_TOOLS,
template=server_config,
install_missing=True,
)
if result is None:
logger.warning("保留自定义 Codex MCP 连接;请手动核对 ARTEMIS 配置。")
return True
except (OSError, ValueError, TypeError):
logger.warning("Codex MCP 同步失败,原配置已保留;请检查 TOML 格式或文件权限。")
return False
def install_rules(client: str, project_root: str) -> list[str]:
"""Compatibility entry: install the official Skill and migrate old marked rules."""
if client != "codex":
raise ValueError("Only Codex MCP configuration is supported")
from artemis.integrations.codex_skill import sync_skill_safely
result = sync_skill_safely(Path(project_root))
return [result["path"]] if result.get("path") else []
def install_mcp_config(client: str, python_exe: str, project_root: str) -> list[str]:
"""Install Codex MCP and the official artemis-use Skill."""
server_cfg = _get_config_snippet(client, python_exe, project_root)["mcp_servers"]["artemis"]
codex_home = Path(os.environ.get("CODEX_HOME") or (Path.home() / ".codex"))
skill_paths = install_rules(client, project_root)
codex_path = codex_home / "config.toml"
if not _merge_codex_toml(codex_path, server_cfg):
raise typer.Exit(1)
return [str(codex_path), *skill_paths]
def mcp_command(
server_type: Annotated[
str,
typer.Option(
"--type",
"-t",
help="Type of MCP server to start: 'agent' (default, universal IDE mobile agent), 'adb' (raw adb), 'xml' (xml fuzzy search).",
),
] = "agent",
transport: Annotated[
str,
typer.Option(
"--transport",
help="MCP transport protocol ('stdio' or 'sse').",
),
] = "stdio",
host: Annotated[
str,
typer.Option(
"--host",
help="Host address when running with SSE transport.",
),
] = "127.0.0.1",
port: Annotated[
int,
typer.Option(
"--port",
"-p",
help="Port number when running with SSE transport.",
),
] = 8001,
install_config: Annotated[
str | None,
typer.Option(
"--install",
"-i",
help="Install ARTEMIS MCP configuration and official artemis-use Skill for Codex: --install codex.",
),
] = None,
generate_config: Annotated[
str | None,
typer.Option(
"--generate-config",
"-g",
help="Output ready-to-use Codex TOML configuration: --generate-config codex.",
),
] = None,
) -> None:
"""Launch or configure the ARTEMIS Model Context Protocol (MCP) server."""
project_root = env_utils.get_project_root()
python_exe = env_utils.resolve_python_executable(project_root)
if install_config:
client = install_config.lower()
if client != "codex":
console.print(
"[bold red]目前仅支持 Codex MCP 配置,请使用 --install codex。[/bold red]"
)
raise typer.Exit(1)
installed_paths = install_mcp_config(client, python_exe, project_root)
console.print(
"[bold green]✔ Successfully installed ARTEMIS MCP server configuration & artemis-use Skill to:[/bold green]"
)
for path in installed_paths:
console.print(f" • [cyan]{path}[/cyan]")
console.print(
"\n[dim]Please restart or reload your IDE window to activate the Artemis MCP tools.[/dim]"
)
raise typer.Exit(0)
if generate_config:
client = generate_config.lower()
if client != "codex":
console.print(
"[bold red]目前仅支持 Codex MCP 配置,请使用 --generate-config codex。[/bold red]"
)
raise typer.Exit(1)
server_cfg = _get_config_snippet(client, python_exe, project_root)["mcp_servers"]["artemis"]
json_str = _codex_toml_block(server_cfg)
syntax_language = "toml"
syntax = Syntax(json_str, syntax_language, theme="monokai", line_numbers=False)
console.print(f"[bold cyan]MCP Configuration for {client.upper()}:[/bold cyan]")
console.print(syntax)
raise typer.Exit(0)
st = server_type.lower()
if st in ("agent", "mobile", "artemis", "default"):
from mcp_server.server import main as bridge_main
bridge_main(transport=transport, host=host, port=port)
return
threading.Thread(target=start_awake_service, daemon=True, name="artemis-awake-init").start()
try:
if st == "adb":
from artemis.mcp.adb_server import mcp as adb_mcp
if transport.lower() == "sse":
logger.info(f"Starting Artemis ADB MCP Server over {transport}...")
adb_mcp.run(transport="sse", host=host, port=port)
else:
# stdio carries the MCP JSON-RPC stream: redirect logging and detach
# from the parent DataEngine before any output can corrupt it.
from artemis.mcp.adb_server import configure_stdio_mode
configure_stdio_mode()
logger.info("Starting Artemis ADB MCP Server over stdio...")
adb_mcp.run(transport="stdio")
elif st == "xml":
from artemis.mcp.xml_search_server import mcp as xml_mcp
logger.info(f"Starting Artemis XML Fuzzy Search MCP Server over {transport}...")
if transport.lower() == "sse":
xml_mcp.run(transport="sse", host=host, port=port)
else:
xml_mcp.run(transport="stdio")
else:
logger.error(
f"Unsupported MCP server type: {server_type}. Use 'agent', 'adb', or 'xml'."
)
raise typer.Exit(1)
finally:
shutdown_awake_service()