Skip to content

Commit 7629789

Browse files
author
Maxime Gravouil
committed
feat(antigravity): support mcp_config.json generation under .agents/
1 parent b915f8d commit 7629789

6 files changed

Lines changed: 139 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- `apm install` now supports target-native MCP config generation for the `antigravity` target, writing to `.agents/mcp_config.json` (project scope) or `~/.gemini/config/mcp_config.json` (user scope). Remote SSE and HTTP servers are correctly formatted to use the `serverUrl` field required by Google Antigravity. (by @okamiconcept, #2039)
13+
14+
1015
## [0.24.0] - 2026-07-05
1116

1217
### Added

src/apm_cli/adapters/client/antigravity.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,20 @@ def _get_config_dir(self) -> Path:
5252
def get_config_path(self):
5353
"""Return the path to ``mcp_config.json`` for the active scope."""
5454
return str(self._get_config_dir() / "mcp_config.json")
55+
56+
def _format_server_config(self, server_info, env_overrides=None, runtime_vars=None):
57+
"""Format server info into Antigravity CLI MCP configuration.
58+
59+
Delegates to the parent (GeminiClientAdapter) for initial formatting,
60+
then replaces any ``url`` or ``httpUrl`` key with ``serverUrl`` for
61+
remote endpoints to match Antigravity's schema.
62+
"""
63+
config = super()._format_server_config(
64+
server_info, env_overrides=env_overrides, runtime_vars=runtime_vars
65+
)
66+
if isinstance(config, dict):
67+
if "url" in config:
68+
config["serverUrl"] = config.pop("url")
69+
elif "httpUrl" in config:
70+
config["serverUrl"] = config.pop("httpUrl")
71+
return config

src/apm_cli/integration/mcp_integrator_install.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ def _discover_installed_runtimes(project_root_path, *, user_scope: bool) -> list
240240
"claude",
241241
"intellij",
242242
"hermes",
243+
"antigravity",
243244
]:
244245
try:
245246
if not _runtime_is_present(
@@ -283,6 +284,10 @@ def _runtime_is_present(
283284
from apm_cli.adapters.client.intellij import _intellij_config_dir
284285

285286
return _intellij_config_dir().is_dir()
287+
if runtime_name == "antigravity":
288+
if user_scope:
289+
return find_runtime_binary("agy") is not None
290+
return (project_root_path / ".agents").is_dir()
286291
if runtime_name == "hermes":
287292
return _hermes_runtime_opted_in()
288293
return manager.is_runtime_available(runtime_name)
@@ -307,6 +312,12 @@ def _discover_installed_runtimes_fallback(
307312
# Claude Code: directory-presence OR binary-on-PATH
308313
if (project_root_path / ".claude").is_dir() or find_runtime_binary("claude") is not None:
309314
installed_runtimes.append("claude")
315+
# Antigravity: directory-presence (project-scope) OR binary-on-PATH (user-scope)
316+
if user_scope:
317+
if find_runtime_binary("agy") is not None:
318+
installed_runtimes.append("antigravity")
319+
elif (project_root_path / ".agents").is_dir():
320+
installed_runtimes.append("antigravity")
310321
# JetBrains Copilot: user-scope config directory presence
311322
try:
312323
from apm_cli.adapters.client.intellij import _intellij_config_dir

src/apm_cli/integration/targets.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ def for_scope(self, user_scope: bool = False) -> TargetProfile | None:
450450
"vscode": "copilot",
451451
"agents": "copilot",
452452
"intellij": "copilot",
453+
"antigravity": "antigravity",
453454
}
454455

455456

tests/integration/test_mcp_targets_gating_e2e.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ def _make_stdio_dep(name: str = "test-srv") -> MCPDependency:
4545
)
4646

4747

48+
def _make_remote_dep(name: str = "test-remote") -> MCPDependency:
49+
"""Build a synthetic remote MCP dependency that needs no network."""
50+
return MCPDependency.from_dict(
51+
{
52+
"name": name,
53+
"registry": False,
54+
"transport": "http",
55+
"url": "https://api.example.com/mcp/",
56+
"headers": {
57+
"Authorization": "Bearer TEST_TOKEN",
58+
},
59+
}
60+
)
61+
62+
4863
def _seed_signal(project: Path, target: str) -> None:
4964
"""Seed an on-disk signal that ``detect_signals`` recognizes."""
5065
if target == "copilot":
@@ -77,6 +92,10 @@ def _codex_mcp_path(project: Path) -> Path:
7792
return project / ".codex" / "config.toml"
7893

7994

95+
def _agents_mcp_path(project: Path) -> Path:
96+
return project / ".agents" / "mcp_config.json"
97+
98+
8099
class TestMCPTargetsGatingE2E:
81100
def test_targets_whitelist_copilot_suppresses_foreign_writes(
82101
self, tmp_path, capsys, monkeypatch
@@ -207,3 +226,44 @@ def test_greenfield_no_targets_no_signals_no_flag_writes_nothing(
207226
"not receive a silent copilot-vscode MCP write -- the "
208227
"pre-#1336 fallback is gone."
209228
)
229+
230+
def test_targets_whitelist_antigravity_allows_listed_runtimes(self, tmp_path, monkeypatch):
231+
"""``targets: [antigravity]`` -> antigravity MCP config IS written
232+
when .agents/ project-scope directory exists.
233+
"""
234+
project = tmp_path / "proj-whitelist-antigravity"
235+
project.mkdir()
236+
fake_home = tmp_path / "home"
237+
fake_home.mkdir()
238+
monkeypatch.setenv("HOME", str(fake_home))
239+
240+
# Seed the .agents directory (the required signal for project scope)
241+
(project / ".agents").mkdir()
242+
243+
MCPIntegrator.install(
244+
[
245+
_make_stdio_dep("e2e-antigravity-stdio"),
246+
_make_remote_dep("e2e-antigravity-remote"),
247+
],
248+
project_root=project,
249+
apm_config={"targets": ["antigravity"]},
250+
)
251+
252+
assert _agents_mcp_path(project).exists(), (
253+
"antigravity MCP config MUST be written when antigravity IS in targets "
254+
"and .agents/ directory exists."
255+
)
256+
agents_data = json.loads(_agents_mcp_path(project).read_text(encoding="utf-8"))
257+
258+
# Verify stdio server formatting
259+
stdio_srv = agents_data.get("mcpServers", {}).get("e2e-antigravity-stdio", {})
260+
assert stdio_srv.get("command") == "echo"
261+
assert stdio_srv.get("args") == ["mcp-targets-gate-e2e"]
262+
263+
# Verify remote server formatting uses serverUrl instead of url or httpUrl
264+
remote_srv = agents_data.get("mcpServers", {}).get("e2e-antigravity-remote", {})
265+
assert "serverUrl" in remote_srv
266+
assert remote_srv["serverUrl"] == "https://api.example.com/mcp/"
267+
assert "url" not in remote_srv
268+
assert "httpUrl" not in remote_srv
269+
assert remote_srv.get("headers", {}).get("Authorization") == "Bearer TEST_TOKEN"

tests/unit/integration/test_antigravity_target.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,51 @@ def test_antigravity_mcp_writes_mcp_servers_to_dedicated_file(tmp_path: Path) ->
267267
assert data["mcpServers"]["demo"]["command"] == "npx"
268268

269269

270+
def test_antigravity_mcp_remote_server_uses_server_url(tmp_path: Path) -> None:
271+
(tmp_path / ".agents").mkdir()
272+
adapter = AntigravityClientAdapter(project_root=tmp_path, user_scope=False)
273+
274+
server_info = {
275+
"name": "remote-demo",
276+
"remotes": [
277+
{
278+
"url": "https://api.example.com/mcp/",
279+
"transport_type": "sse",
280+
}
281+
],
282+
}
283+
server_config = adapter._format_server_config(server_info)
284+
assert "serverUrl" in server_config
285+
assert server_config["serverUrl"] == "https://api.example.com/mcp/"
286+
assert "url" not in server_config
287+
assert "httpUrl" not in server_config
288+
289+
290+
def test_antigravity_mcp_runtime_is_detected_when_agents_dir_exists(tmp_path: Path) -> None:
291+
from apm_cli.integration.mcp_integrator_install import _discover_installed_runtimes
292+
293+
# Without .agents, not detected
294+
assert "antigravity" not in _discover_installed_runtimes(tmp_path, user_scope=False)
295+
296+
# With .agents, detected
297+
(tmp_path / ".agents").mkdir()
298+
assert "antigravity" in _discover_installed_runtimes(tmp_path, user_scope=False)
299+
300+
301+
def test_antigravity_mcp_runtime_is_detected_at_user_scope_when_agy_binary_exists(
302+
monkeypatch,
303+
) -> None:
304+
import shutil
305+
306+
from apm_cli.integration.mcp_integrator_install import _discover_installed_runtimes
307+
308+
# Mock shutil.which to find 'agy'
309+
monkeypatch.setattr(shutil, "which", lambda cmd: "/usr/local/bin/agy" if cmd == "agy" else None)
310+
311+
# At user scope, should detect antigravity since 'agy' binary exists
312+
assert "antigravity" in _discover_installed_runtimes(Path("/nonexistent"), user_scope=True)
313+
314+
270315
# ---------------------------------------------------------------------------
271316
# Hooks -> .agents/hooks.json in Antigravity's OWN native schema
272317
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)