272 lines
9.6 KiB
Python
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()
|