From 0ae6e452f828cb41b070cbab99b646fcf8fc991b Mon Sep 17 00:00:00 2001 From: Roni Ku Date: Mon, 1 Jun 2026 10:31:55 +0300 Subject: [PATCH 1/2] CM-65100-add-file-support-codex-and-claude --- .../apps/ai_guardrails/ides/claude_code.py | 1 + cycode/cli/apps/ai_guardrails/ides/codex.py | 1 + .../ai_guardrails/ides/test_claude_code.py | 37 ++++++++++++++- .../commands/ai_guardrails/ides/test_codex.py | 47 +++++++++++++++++++ .../test_session_start_command.py | 2 + tmp3.json | 0 6 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tmp3.json diff --git a/cycode/cli/apps/ai_guardrails/ides/claude_code.py b/cycode/cli/apps/ai_guardrails/ides/claude_code.py index 4178ec56..eaa1edd6 100644 --- a/cycode/cli/apps/ai_guardrails/ides/claude_code.py +++ b/cycode/cli/apps/ai_guardrails/ides/claude_code.py @@ -188,6 +188,7 @@ def _read_claude_plugin(plugin_dir: Path) -> tuple[dict, dict]: servers: dict = mcp_config.get('mcpServers') or {} if servers: entry['mcp_server_names'] = list(servers.keys()) + entry['mcp_config_file'] = json.dumps(mcp_config) return entry, servers diff --git a/cycode/cli/apps/ai_guardrails/ides/codex.py b/cycode/cli/apps/ai_guardrails/ides/codex.py index f8c9b04d..a3ba79fa 100644 --- a/cycode/cli/apps/ai_guardrails/ides/codex.py +++ b/cycode/cli/apps/ai_guardrails/ides/codex.py @@ -135,6 +135,7 @@ def _read_codex_plugin(plugin_dir: Path) -> tuple[dict, dict]: servers = {} if servers: entry['mcp_server_names'] = list(servers.keys()) + entry['mcp_config_file'] = json.dumps(mcp_doc) return entry, servers diff --git a/tests/cli/commands/ai_guardrails/ides/test_claude_code.py b/tests/cli/commands/ai_guardrails/ides/test_claude_code.py index f997abe3..536f0bea 100644 --- a/tests/cli/commands/ai_guardrails/ides/test_claude_code.py +++ b/tests/cli/commands/ai_guardrails/ides/test_claude_code.py @@ -8,7 +8,12 @@ from pytest_mock import MockerFixture from cycode.cli.apps.ai_guardrails.ides.base import HookDecision -from cycode.cli.apps.ai_guardrails.ides.claude_code import ClaudeCode, _email_from_config, load_claude_config +from cycode.cli.apps.ai_guardrails.ides.claude_code import ( + ClaudeCode, + _email_from_config, + _read_claude_plugin, + load_claude_config, +) from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType @@ -214,6 +219,36 @@ def test_email_none_when_no_oauth(mocker: MockerFixture) -> None: assert unified.ide_user_email is None +# _read_claude_plugin + + +def test_read_claude_plugin_includes_mcp_config_file(tmp_path: Path) -> None: + mcp_content = {'mcpServers': {'aspire': {'command': 'aspire', 'args': ['mcp', 'start']}}} + (tmp_path / '.mcp.json').write_text(json.dumps(mcp_content)) + + entry, servers = _read_claude_plugin(tmp_path) + + assert 'mcp_config_file' in entry + assert json.loads(entry['mcp_config_file']) == mcp_content + assert servers == mcp_content['mcpServers'] + + +def test_read_claude_plugin_no_mcp_config_file_when_no_servers(tmp_path: Path) -> None: + (tmp_path / '.mcp.json').write_text(json.dumps({'mcpServers': {}})) + + entry, servers = _read_claude_plugin(tmp_path) + + assert 'mcp_config_file' not in entry + assert servers == {} + + +def test_read_claude_plugin_no_mcp_config_file_when_missing(tmp_path: Path) -> None: + entry, servers = _read_claude_plugin(tmp_path) + + assert 'mcp_config_file' not in entry + assert servers == {} + + # Session context diff --git a/tests/cli/commands/ai_guardrails/ides/test_codex.py b/tests/cli/commands/ai_guardrails/ides/test_codex.py index f71e19a2..dc67ddf2 100644 --- a/tests/cli/commands/ai_guardrails/ides/test_codex.py +++ b/tests/cli/commands/ai_guardrails/ides/test_codex.py @@ -16,6 +16,7 @@ _email_from_auth, _enable_codex_hooks_feature, _load_codex_config, + _read_codex_plugin, ) from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType @@ -333,3 +334,49 @@ def test_session_context_no_config() -> None: servers, plugins = Codex().get_session_context() assert servers == {} assert plugins == {} + + +def _write_codex_plugin(plugin_dir: Path, mcp_doc: dict) -> None: + """Lay out a Codex plugin: manifest referencing .mcp.json + the MCP file itself.""" + (plugin_dir / '.codex-plugin').mkdir(parents=True, exist_ok=True) + (plugin_dir / '.codex-plugin' / 'plugin.json').write_text( + json.dumps({'name': 'demo', 'mcpServers': '.mcp.json'}) + ) + (plugin_dir / '.mcp.json').write_text(json.dumps(mcp_doc)) + + +def test_read_codex_plugin_includes_mcp_config_file(tmp_path: Path) -> None: + mcp_content = {'mcpServers': {'dummy-server': {'command': 'dummy-command', 'args': ['serve']}}} + _write_codex_plugin(tmp_path, mcp_content) + + entry, servers = _read_codex_plugin(tmp_path) + + assert json.loads(entry['mcp_config_file']) == mcp_content + assert servers == mcp_content['mcpServers'] + + +def test_read_codex_plugin_mcp_config_file_bare_map(tmp_path: Path) -> None: + # Codex MCP files may be a bare {name: cfg} map with no mcpServers wrapper. + mcp_content = {'dummy-server': {'command': 'dummy-command'}} + _write_codex_plugin(tmp_path, mcp_content) + + entry, servers = _read_codex_plugin(tmp_path) + + assert json.loads(entry['mcp_config_file']) == mcp_content + assert servers == mcp_content + + +def test_read_codex_plugin_no_mcp_config_file_when_no_servers(tmp_path: Path) -> None: + _write_codex_plugin(tmp_path, {'mcpServers': {}}) + + entry, servers = _read_codex_plugin(tmp_path) + + assert 'mcp_config_file' not in entry + assert servers == {} + + +def test_read_codex_plugin_no_mcp_config_file_when_no_manifest(tmp_path: Path) -> None: + entry, servers = _read_codex_plugin(tmp_path) + + assert 'mcp_config_file' not in entry + assert servers == {} diff --git a/tests/cli/commands/ai_guardrails/test_session_start_command.py b/tests/cli/commands/ai_guardrails/test_session_start_command.py index 0ae57226..63fd1a9a 100644 --- a/tests/cli/commands/ai_guardrails/test_session_start_command.py +++ b/tests/cli/commands/ai_guardrails/test_session_start_command.py @@ -282,6 +282,7 @@ def test_claude_code_merges_plugin_mcp_servers_and_metadata( with patch('sys.stdin', new=StringIO(json.dumps(payload))): session_start_command(mock_ctx, ide='claude-code') + plugin_mcp = {'mcpServers': {'aspire': {'command': 'aspire', 'args': ['mcp', 'start']}}} mock_ai_client.report_session_context.assert_called_once_with( mcp_servers={ 'gitlab': {'command': 'npx'}, @@ -294,6 +295,7 @@ def test_claude_code_merges_plugin_mcp_servers_and_metadata( 'version': '1.0.28', 'description': 'Shared skills', 'mcp_server_names': ['aspire'], + 'mcp_config_file': json.dumps(plugin_mcp), } }, user_email=None, diff --git a/tmp3.json b/tmp3.json new file mode 100644 index 00000000..e69de29b From 8262f1ef3e8a06f8e39261136dba68c48d4d0bc7 Mon Sep 17 00:00:00 2001 From: Roni Ku Date: Tue, 2 Jun 2026 15:56:41 +0300 Subject: [PATCH 2/2] CM-65100-remove-test-file --- tmp3.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tmp3.json diff --git a/tmp3.json b/tmp3.json deleted file mode 100644 index e69de29b..00000000