[project] name = "ios-mcp" version = "0.1.1" description = "MCP server that lets AI agents drive iOS devices and simulators via XCUIAutomation/WebDriverAgent" readme = "README.md" requires-python = ">=3.12" license = { text = "MIT" } authors = [{ name = "Ehsan Mazaheri" }] keywords = ["mcp", "ios", "iphone", "simulator", "agent", "llm", "automation", "xcuitest"] # macOS only, and stated up front: this builds on Xcode, simctl and # WebDriverAgent. A pip install that succeeds on Linux and then cannot do # anything is a worse first impression than a clear platform classifier. classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: MacOS :: MacOS X", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Quality Assurance", "Typing :: Typed", ] dependencies = [ "fastmcp>=3.0.0", "httpx>=0.28", "pydantic>=2.9", "pydantic-settings>=2.6", "anyio>=4.6", ] [project.optional-dependencies] vision = ["pillow>=11.0"] # One repository, three distributions, and the dependencies only ever point # one way: `ios-tui` -> `ios-agent` -> `ios-mcp`. That is what keeps `ios-mcp` # usable by any agent rather than only by the one shipped beside it, and what # keeps `ios-agent` usable by any front end rather than only by this one. # # The front end is a separate distribution rather than a subpackage of the # agent because the agent is held to a seven-module public surface of `ios-mcp` # (tests/unit/test_layering.py) and a front end needs device discovery and the # doctor, which are outside it. See docs/adr/0008. [tool.uv.workspace] members = ["agent", "tui"] [tool.uv.sources] ios-agent = { workspace = true } ios-mcp = { workspace = true } ios-tui = { workspace = true } [project.scripts] ios-mcp = "ios_mcp.__main__:main" [dependency-groups] dev = [ "pytest>=8.3", "pytest-asyncio>=0.24", "pytest-cov>=6.0", "ruff>=0.8", "mypy>=1.13", "respx>=0.22", "pillow>=12.3.0", # Installed for the test run so the eval suite can drive both, not because # ios-mcp needs it. Both provider extras are here so the evals can be run # against either without a resync; the agent itself commits to none, and # `IOS_AGENT_PROVIDER` picks one at runtime. "ios-agent[anthropic,openai]", # Installed so `tests/tui` can import `ios_tui` without a `pythonpath` # entry. The front end is a real distribution, so it is installed like one # rather than reached through a path hack. "ios-tui", ] [project.urls] Homepage = "https://github.com/emazaheri/ios-agent" Repository = "https://github.com/emazaheri/ios-agent" Issues = "https://github.com/emazaheri/ios-agent/issues" Documentation = "https://github.com/emazaheri/ios-agent#readme" Changelog = "https://github.com/emazaheri/ios-agent/releases" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["ios_mcp"] # An allow-list, not exclusions. A source distribution defaults to "everything # in the directory", which here meant the private notes under .claude/, a # 2.6 MB docs/images, and scripts that are not part of the package. Naming what # ships is the only version of this that stays correct as the tree grows. [tool.hatch.build.targets.sdist] include = [ "ios_mcp", "pyproject.toml", "server.json", "README.md", "LICENSE", "SAFETY.md", "SECURITY.md", "ARCHITECTURE.md", ".env.example", "docs/real-device-setup.md", "docs/tool-reference.md", "docs/adr", "scripts/prepare_wda.sh", "scripts/start_tunnel.sh", ] [tool.pytest.ini_options] asyncio_mode = "auto" # One event loop for the whole run. The device pool and its WebDriverAgent # subprocesses live across tests, and tearing them down on a different loop # than the one that created them raises "attached to a different loop". asyncio_default_fixture_loop_scope = "session" asyncio_default_test_loop_scope = "session" testpaths = ["tests"] # Test helper modules (fake_wda, trees, harness, flows) live beside the tests # that use them rather than in the package. pythonpath = ["tests", "tests/evals", "tests/evals/agent", "tests/tui", "scripts"] markers = [ "simulator: requires a booted iOS Simulator (skipped when unavailable)", "device: requires an attached physical iPhone (skipped when unavailable)", "eval: golden-flow evaluation, slow", "agent: goal-directed agent evaluation against a scripted device", "device_state: configures the scripted device for one MCP-backend test", "model: calls a real model, needs credentials and costs money", ] [tool.ruff] line-length = 100 target-version = "py312" [tool.ruff.lint] select = ["E", "F", "I", "UP", "B", "SIM", "RUF"] [[tool.mypy.overrides]] module = ["PIL.*"] ignore_missing_imports = true [tool.mypy] python_version = "3.12" strict = true warn_return_any = true disallow_untyped_defs = true