Navegador

feat: editor integrations — MCP config generation for Claude Code, Cursor, Codex, Windsurf CLI: navegador editor setup <editor> [--write]. Generates correct MCP config snippets for each AI editor, with optional file write. Closes #27

lmata 2026-03-23 04:59 trunk
Commit 8b1274310a7a023d727b774738858d77f5cb5e88e6c82877d2e4c2df80d767e8
--- a/navegador/editor.py
+++ b/navegador/editor.py
@@ -0,0 +1,39 @@
1
+"""
2
+Editor integrations — generate MCP config snippets for AI coding editors.
3
+
4
+Supported editors:
5
+ claude-code — .claude/mcp.json
6
+ cursor — .cursor/mcp.json
7
+ codex — .codex/config.json
8
+ windsurf — .windsurf/mcp.json
9
+"""
10
+
11
+from __future__ import annotations
12
+
13
+import json
14
+from pathlib import Path
15
+
16
+SUPPORTED_EDITORS = ["claude-code", "cursor", "codex", "windsurf"]
17
+
18
+# Config file path relative to the project root for each editor
19
+_CONFIG_PATHS: dict[str, str] = {
20
+ "claude-code": ".claude/mcp.json",
21
+ "cursor": ".cursor/mcp.json",
22
+ "codex": ".codex/config.json",
23
+ "windsurf": ".windsurf/mcp.json",
24
+}
25
+
26
+
27
+def _mcp_block(db: str) -> dict:
28
+ """Return the shared mcpServers block used by all editors."""
29
+ return {
30
+ "mcpServers": {
31
+ "navegador": {
32
+ "command": "navegador",
33
+ "args": ["mcp", "--db", db],
34
+ }
35
+ }
36
+ }
37
+
38
+
39
+cla
--- a/navegador/editor.py
+++ b/navegador/editor.py
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/navegador/editor.py
+++ b/navegador/editor.py
@@ -0,0 +1,39 @@
1 """
2 Editor integrations — generate MCP config snippets for AI coding editors.
3
4 Supported editors:
5 claude-code — .claude/mcp.json
6 cursor — .cursor/mcp.json
7 codex — .codex/config.json
8 windsurf — .windsurf/mcp.json
9 """
10
11 from __future__ import annotations
12
13 import json
14 from pathlib import Path
15
16 SUPPORTED_EDITORS = ["claude-code", "cursor", "codex", "windsurf"]
17
18 # Config file path relative to the project root for each editor
19 _CONFIG_PATHS: dict[str, str] = {
20 "claude-code": ".claude/mcp.json",
21 "cursor": ".cursor/mcp.json",
22 "codex": ".codex/config.json",
23 "windsurf": ".windsurf/mcp.json",
24 }
25
26
27 def _mcp_block(db: str) -> dict:
28 """Return the shared mcpServers block used by all editors."""
29 return {
30 "mcpServers": {
31 "navegador": {
32 "command": "navegador",
33 "args": ["mcp", "--db", db],
34 }
35 }
36 }
37
38
39 cla
--- a/tests/test_editor.py
+++ b/tests/test_editor.py
@@ -0,0 +1,218 @@
1
+"""Tests for editor integration — EditorIntegration class and CLI command."""
2
+
3
+import json
4
+from pathlib import Path
5
+from unittest.mock import MagicMock, patch
6
+
7
+import pytest
8
+from click.testing import CliRunner
9
+
10
+from navegador.cli.commands import main
11
+from navegador.editor import SUPPORTED_EDITORS, EditorIntegration
12
+
13
+# ── EditorIntegration unit tests ──────────────────────────────────────────────
14
+
15
+
16
+class TestEditorIntegration:
17
+ def setup_method(self):
18
+ self.integration = EditorIntegration(db=".navegador/graph.db")
19
+
20
+ # config_for
21
+
22
+ def test_config_for_claude_code(self):
23
+ cfg = self.integration.config_for("claude-code")
24
+ assert cfg["mcpServers"]["navegador"]["command"] == "navegador"
25
+ assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"]
26
+
27
+ def test_config_for_cursor(self):
28
+ cfg = self.integration.config_for("cursor")
29
+ assert cfg["mcpServers"]["navegador"]["command"] == "navegador"
30
+ assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"]
31
+
32
+ def test_config_for_codex(self):
33
+ cfg = self.integration.config_for("codex")
34
+ assert cfg["mcpServers"]["navegador"]["command"] == "navegador"
35
+ assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"]
36
+
37
+ def test_config_for_windsurf(self):
38
+ cfg = self.integration.config_for("windsurf")
39
+ assert cfg["mcpServers"]["navegador"]["command"] == "navegador"
40
+ assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"]
41
+
42
+ def test_config_for_invalid_editor_raises(self):
43
+ with pytest.raises(ValueError, match="Unsupported editor"):
44
+ self.integration.config_for("vscode")
45
+
46
+ # custom db path
47
+
48
+ def test_custom_db_path_reflected_in_config(self):
49
+ integration = EditorIntegration(db="/custom/path/graph.db")
50
+ cfg = integration.config_for("cursor")
51
+ assert cfg["mcpServers"]["navegador"]["args"][2] == "/custom/path/graph.db"
52
+
53
+ # config_json
54
+
55
+ def test_config_json_is_valid_json(self):
56
+ raw = self.integration.config_json("claude-code")
57
+ parsed = json.loads(raw)
58
+ assert "mcpServers" in parsed
59
+
60
+ def test_config_json_is_pretty_printed(self):
61
+ raw = self.integration.config_json("cursor")
62
+ assert "\n" in raw # indented
63
+
64
+ # config_path
65
+
66
+ def test_config_path_claude_code(self):
67
+ assert self.integration.config_path("claude-code") == ".claude/mcp.json"
68
+
69
+ def test_config_path_cursor(self):
70
+ assert self.integration.config_path("cursor") == ".cursor/mcp.json"
71
+
72
+ def test_config_path_codex(self):
73
+ assert self.integration.config_path("codex") == ".codex/config.json"
74
+
75
+ def test_config_path_windsurf(self):
76
+ assert self.integration.config_path("windsurf") == ".windsurf/mcp.json"
77
+
78
+ def test_config_path_invalid_editor_raises(self):
79
+ with pytest.raises(ValueError, match="Unsupported editor"):
80
+ self.integration.config_path("sublime")
81
+
82
+ # write_config
83
+
84
+ def test_write_config_creates_file(self, tmp_path):
85
+ written = self.integration.write_config("claude-code", base_dir=str(tmp_path))
86
+ assert written.exists()
87
+ parsed = json.loads(written.read_text())
88
+ assert "mcpServers" in parsed
89
+
90
+ def test_write_config_creates_parent_dirs(self, tmp_path):
91
+ written = self.integration.write_config("windsurf", base_dir=str(tmp_path))
92
+ assert (tmp_path / ".windsurf").is_dir()
93
+ assert written.name == "mcp.json"
94
+
95
+ def test_write_config_returns_path_object(self, tmp_path):
96
+ result = self.integration.write_config("cursor", base_dir=str(tmp_path))
97
+ assert isinstance(result, Path)
98
+
99
+ def test_write_config_content_matches_config_json(self, tmp_path):
100
+ written = self.integration.write_config("codex", base_dir=str(tmp_path))
101
+ assert written.read_text() == self.integration.config_json("codex")
102
+
103
+ # all editors covered
104
+
105
+ def test_all_editors_supported(self):
106
+ for ed in SUPPORTED_EDITORS:
107
+ cfg = self.integration.config_for(ed)
108
+ assert "mcpServers" in cfg
109
+
110
+
111
+# ── CLI tests ─────────────────────────────────────────────────────────────────
112
+
113
+
114
+class TestEditorSetupCommand:
115
+ # Basic output
116
+
117
+ def test_claude_code_outputs_json(self):
118
+ runner = CliRunner()
119
+ result = runner.invoke(main, ["editor", "setup", "claude-code"])
120
+ assert result.exit_code == 0
121
+ parsed = json.loads(result.output)
122
+ assert "mcpServers" in parsed
123
+ assert parsed["mcpServers"]["navegador"]["command"] == "navegador"
124
+
125
+ def test_cursor_outputs_json(self):
126
+ runner = CliRunner()
127
+ result = runner.invoke(main, ["editor", "setup", "cursor"])
128
+ assert result.exit_code == 0
129
+ parsed = json.loads(result.output)
130
+ assert "mcpServers" in parsed
131
+
132
+ def test_codex_outputs_json(self):
133
+ runner = CliRunner()
134
+ result = runner.invoke(main, ["editor", "setup", "codex"])
135
+ assert result.exit_code == 0
136
+ parsed = json.loads(result.output)
137
+ assert "mcpServers" in parsed
138
+
139
+ def test_windsurf_outputs_json(self):
140
+ runner = CliRunner()
141
+ result = runner.invoke(main, ["editor", "setup", "windsurf"])
142
+ assert result.exit_code == 0
143
+ parsed = json.loads(result.output)
144
+ assert "mcpServers" in parsed
145
+
146
+ # --db option
147
+
148
+ def test_custom_db_reflected_in_output(self):
149
+ runner = CliRunner()
150
+ result = runner.invoke(main, ["editor", "setup", "cursor", "--db", "/custom/graph.db"])
151
+ assert result.exit_code == 0
152
+ parsed = json.loads(result.output)
153
+ assert parsed["mcpServers"]["navegador"]["args"][2] == "/custom/graph.db"
154
+
155
+ # 'all' generates for all editors
156
+
157
+ def test_all_generates_for_all_editors(self):
158
+ runner = CliRunner()
159
+ result = runner.invoke(main, ["editor", "setup", "all"])
160
+ assert result.exit_code == 0
161
+ # Each editor name should appear in the output header
162
+ for ed in SUPPORTED_EDITORS:
163
+ assert ed in result.output
164
+
165
+ def test_all_output_contains_multiple_json_blocks(self):
166
+ runner = CliRunner()
167
+ result = runner.invoke(main, ["editor", "setup", "all"])
168
+ assert result.exit_code == 0
169
+ # Count occurrences of "mcpServers" — one per editor
170
+ assert result.output.count("mcpServers") == len(SUPPORTED_EDITORS)
171
+
172
+ # Invalid editor name
173
+
174
+ def test_invalid_editor_exits_nonzero(self):
175
+ runner = CliRunner()
176
+ result = runner.invoke(main, ["editor", "setup", "vscode"])
177
+ assert result.exit_code != 0
178
+
179
+ def test_invalid_editor_shows_error(self):
180
+ runner = CliRunner()
181
+ result = runner.invoke(main, ["editor", "setup", "vim"])
182
+ assert "vim" in result.output or "vim" in (result.exception or "")
183
+
184
+ # --write flag
185
+
186
+ def test_write_flag_creates_file(self):
187
+ runner = CliRunner()
188
+ with runner.isolated_filesystem():
189
+ result = runner.invoke(main, ["editor", "setup", "claude-code", "--write"])
190
+ assert result.exit_code == 0
191
+ written = Path(".claude/mcp.json")
192
+ assert written.exists()
193
+ parsed = json.loads(written.read_text())
194
+ assert "mcpServers" in parsed
195
+
196
+ def test_write_flag_cursor_creates_correct_file(self):
197
+ runner = CliRunner()
198
+ with runner.isolated_filesystem():
199
+ result = runner.invoke(main, ["editor", "setup", "cursor", "--write"])
200
+ assert result.exit_code == 0
201
+ assert Path(".cursor/mcp.json").exists()
202
+
203
+ def test_write_flag_all_creates_all_files(self):
204
+ runner = CliRunner()
205
+ with runner.isolated_filesystem():
206
+ result = runner.invoke(main, ["editor", "setup", "all", "--write"])
207
+ assert result.exit_code == 0
208
+ assert Path(".claude/mcp.json").exists()
209
+ assert Path(".cursor/mcp.json").exists()
210
+ assert Path(".codex/config.json").exists()
211
+ assert Path(".windsurf/mcp.json").exists()
212
+
213
+ def test_write_flag_shows_written_path(self):
214
+ runner = CliRunner()
215
+ with runner.isolated_filesystem():
216
+ result = runner.invoke(main, ["editor", "setup", "windsurf", "--write"])
217
+ assert result.exit_code == 0
218
+ assert "Written" in result.output or ".windsurf" in result.output
--- a/tests/test_editor.py
+++ b/tests/test_editor.py
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/tests/test_editor.py
+++ b/tests/test_editor.py
@@ -0,0 +1,218 @@
1 """Tests for editor integration — EditorIntegration class and CLI command."""
2
3 import json
4 from pathlib import Path
5 from unittest.mock import MagicMock, patch
6
7 import pytest
8 from click.testing import CliRunner
9
10 from navegador.cli.commands import main
11 from navegador.editor import SUPPORTED_EDITORS, EditorIntegration
12
13 # ── EditorIntegration unit tests ──────────────────────────────────────────────
14
15
16 class TestEditorIntegration:
17 def setup_method(self):
18 self.integration = EditorIntegration(db=".navegador/graph.db")
19
20 # config_for
21
22 def test_config_for_claude_code(self):
23 cfg = self.integration.config_for("claude-code")
24 assert cfg["mcpServers"]["navegador"]["command"] == "navegador"
25 assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"]
26
27 def test_config_for_cursor(self):
28 cfg = self.integration.config_for("cursor")
29 assert cfg["mcpServers"]["navegador"]["command"] == "navegador"
30 assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"]
31
32 def test_config_for_codex(self):
33 cfg = self.integration.config_for("codex")
34 assert cfg["mcpServers"]["navegador"]["command"] == "navegador"
35 assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"]
36
37 def test_config_for_windsurf(self):
38 cfg = self.integration.config_for("windsurf")
39 assert cfg["mcpServers"]["navegador"]["command"] == "navegador"
40 assert cfg["mcpServers"]["navegador"]["args"] == ["mcp", "--db", ".navegador/graph.db"]
41
42 def test_config_for_invalid_editor_raises(self):
43 with pytest.raises(ValueError, match="Unsupported editor"):
44 self.integration.config_for("vscode")
45
46 # custom db path
47
48 def test_custom_db_path_reflected_in_config(self):
49 integration = EditorIntegration(db="/custom/path/graph.db")
50 cfg = integration.config_for("cursor")
51 assert cfg["mcpServers"]["navegador"]["args"][2] == "/custom/path/graph.db"
52
53 # config_json
54
55 def test_config_json_is_valid_json(self):
56 raw = self.integration.config_json("claude-code")
57 parsed = json.loads(raw)
58 assert "mcpServers" in parsed
59
60 def test_config_json_is_pretty_printed(self):
61 raw = self.integration.config_json("cursor")
62 assert "\n" in raw # indented
63
64 # config_path
65
66 def test_config_path_claude_code(self):
67 assert self.integration.config_path("claude-code") == ".claude/mcp.json"
68
69 def test_config_path_cursor(self):
70 assert self.integration.config_path("cursor") == ".cursor/mcp.json"
71
72 def test_config_path_codex(self):
73 assert self.integration.config_path("codex") == ".codex/config.json"
74
75 def test_config_path_windsurf(self):
76 assert self.integration.config_path("windsurf") == ".windsurf/mcp.json"
77
78 def test_config_path_invalid_editor_raises(self):
79 with pytest.raises(ValueError, match="Unsupported editor"):
80 self.integration.config_path("sublime")
81
82 # write_config
83
84 def test_write_config_creates_file(self, tmp_path):
85 written = self.integration.write_config("claude-code", base_dir=str(tmp_path))
86 assert written.exists()
87 parsed = json.loads(written.read_text())
88 assert "mcpServers" in parsed
89
90 def test_write_config_creates_parent_dirs(self, tmp_path):
91 written = self.integration.write_config("windsurf", base_dir=str(tmp_path))
92 assert (tmp_path / ".windsurf").is_dir()
93 assert written.name == "mcp.json"
94
95 def test_write_config_returns_path_object(self, tmp_path):
96 result = self.integration.write_config("cursor", base_dir=str(tmp_path))
97 assert isinstance(result, Path)
98
99 def test_write_config_content_matches_config_json(self, tmp_path):
100 written = self.integration.write_config("codex", base_dir=str(tmp_path))
101 assert written.read_text() == self.integration.config_json("codex")
102
103 # all editors covered
104
105 def test_all_editors_supported(self):
106 for ed in SUPPORTED_EDITORS:
107 cfg = self.integration.config_for(ed)
108 assert "mcpServers" in cfg
109
110
111 # ── CLI tests ─────────────────────────────────────────────────────────────────
112
113
114 class TestEditorSetupCommand:
115 # Basic output
116
117 def test_claude_code_outputs_json(self):
118 runner = CliRunner()
119 result = runner.invoke(main, ["editor", "setup", "claude-code"])
120 assert result.exit_code == 0
121 parsed = json.loads(result.output)
122 assert "mcpServers" in parsed
123 assert parsed["mcpServers"]["navegador"]["command"] == "navegador"
124
125 def test_cursor_outputs_json(self):
126 runner = CliRunner()
127 result = runner.invoke(main, ["editor", "setup", "cursor"])
128 assert result.exit_code == 0
129 parsed = json.loads(result.output)
130 assert "mcpServers" in parsed
131
132 def test_codex_outputs_json(self):
133 runner = CliRunner()
134 result = runner.invoke(main, ["editor", "setup", "codex"])
135 assert result.exit_code == 0
136 parsed = json.loads(result.output)
137 assert "mcpServers" in parsed
138
139 def test_windsurf_outputs_json(self):
140 runner = CliRunner()
141 result = runner.invoke(main, ["editor", "setup", "windsurf"])
142 assert result.exit_code == 0
143 parsed = json.loads(result.output)
144 assert "mcpServers" in parsed
145
146 # --db option
147
148 def test_custom_db_reflected_in_output(self):
149 runner = CliRunner()
150 result = runner.invoke(main, ["editor", "setup", "cursor", "--db", "/custom/graph.db"])
151 assert result.exit_code == 0
152 parsed = json.loads(result.output)
153 assert parsed["mcpServers"]["navegador"]["args"][2] == "/custom/graph.db"
154
155 # 'all' generates for all editors
156
157 def test_all_generates_for_all_editors(self):
158 runner = CliRunner()
159 result = runner.invoke(main, ["editor", "setup", "all"])
160 assert result.exit_code == 0
161 # Each editor name should appear in the output header
162 for ed in SUPPORTED_EDITORS:
163 assert ed in result.output
164
165 def test_all_output_contains_multiple_json_blocks(self):
166 runner = CliRunner()
167 result = runner.invoke(main, ["editor", "setup", "all"])
168 assert result.exit_code == 0
169 # Count occurrences of "mcpServers" — one per editor
170 assert result.output.count("mcpServers") == len(SUPPORTED_EDITORS)
171
172 # Invalid editor name
173
174 def test_invalid_editor_exits_nonzero(self):
175 runner = CliRunner()
176 result = runner.invoke(main, ["editor", "setup", "vscode"])
177 assert result.exit_code != 0
178
179 def test_invalid_editor_shows_error(self):
180 runner = CliRunner()
181 result = runner.invoke(main, ["editor", "setup", "vim"])
182 assert "vim" in result.output or "vim" in (result.exception or "")
183
184 # --write flag
185
186 def test_write_flag_creates_file(self):
187 runner = CliRunner()
188 with runner.isolated_filesystem():
189 result = runner.invoke(main, ["editor", "setup", "claude-code", "--write"])
190 assert result.exit_code == 0
191 written = Path(".claude/mcp.json")
192 assert written.exists()
193 parsed = json.loads(written.read_text())
194 assert "mcpServers" in parsed
195
196 def test_write_flag_cursor_creates_correct_file(self):
197 runner = CliRunner()
198 with runner.isolated_filesystem():
199 result = runner.invoke(main, ["editor", "setup", "cursor", "--write"])
200 assert result.exit_code == 0
201 assert Path(".cursor/mcp.json").exists()
202
203 def test_write_flag_all_creates_all_files(self):
204 runner = CliRunner()
205 with runner.isolated_filesystem():
206 result = runner.invoke(main, ["editor", "setup", "all", "--write"])
207 assert result.exit_code == 0
208 assert Path(".claude/mcp.json").exists()
209 assert Path(".cursor/mcp.json").exists()
210 assert Path(".codex/config.json").exists()
211 assert Path(".windsurf/mcp.json").exists()
212
213 def test_write_flag_shows_written_path(self):
214 runner = CliRunner()
215 with runner.isolated_filesystem():
216 result = runner.invoke(main, ["editor", "setup", "windsurf", "--write"])
217 assert result.exit_code == 0
218 assert "Written" in result.output or ".windsurf" in result.output

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button