Skip to content

Commit 262dd73

Browse files
committed
CM-61986-remove-mcp-server-extraction
1 parent ae57fa0 commit 262dd73

5 files changed

Lines changed: 57 additions & 138 deletions

File tree

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Reader for ~/.claude.json configuration file.
22
3-
Extracts user email and MCP server configuration from the Claude Code
4-
global config file for use in AI guardrails scan enrichment.
3+
Extracts user email from the Claude Code global config file
4+
for use in AI guardrails scan enrichment.
55
"""
66

77
import json
@@ -14,11 +14,6 @@
1414

1515
_CLAUDE_CONFIG_PATH = Path.home() / '.claude.json'
1616

17-
_SERVER_TYPE_MAPPING = {
18-
'stdio': 'Local',
19-
'http': 'Remote',
20-
}
21-
2217

2318
def load_claude_config(config_path: Optional[Path] = None) -> Optional[dict]:
2419
"""Load and parse ~/.claude.json.
@@ -47,40 +42,3 @@ def get_user_email(config: dict) -> Optional[str]:
4742
Reads oauthAccount.emailAddress from the config dict.
4843
"""
4944
return config.get('oauthAccount', {}).get('emailAddress')
50-
51-
52-
def get_mcp_servers(config: dict) -> list[dict]:
53-
"""Extract top-level MCP servers from Claude config and map to backend DTO shape.
54-
55-
Reads the top-level mcpServers dict and transforms each entry to match
56-
the McpServerPayloadDTO expected by ai-security-manager:
57-
- name: server key name
58-
- server_type: "Local" (stdio) or "Remote" (http)
59-
- command: executable command (stdio servers)
60-
- args: command arguments (stdio servers)
61-
- url: server URL (http servers)
62-
63-
Returns:
64-
List of dicts matching McpServerPayloadDTO shape. Empty list if none found.
65-
"""
66-
mcp_servers = config.get('mcpServers', {})
67-
if not isinstance(mcp_servers, dict):
68-
return []
69-
70-
result = []
71-
for name, server_config in mcp_servers.items():
72-
if not isinstance(server_config, dict):
73-
continue
74-
75-
raw_type = server_config.get('type', '')
76-
server_type = _SERVER_TYPE_MAPPING.get(raw_type, raw_type)
77-
78-
result.append({
79-
'name': name,
80-
'server_type': server_type,
81-
'command': server_config.get('command'),
82-
'args': server_config.get('args'),
83-
'url': server_config.get('url'),
84-
})
85-
86-
return result

cycode/cli/apps/ai_guardrails/scan/scan_command.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import typer
1818

1919
from cycode.cli.apps.ai_guardrails.consts import AIIDEType
20-
from cycode.cli.apps.ai_guardrails.scan.claude_config import get_mcp_servers, load_claude_config
2120
from cycode.cli.apps.ai_guardrails.scan.handlers import get_handler_for_event
2221
from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
2322
from cycode.cli.apps.ai_guardrails.scan.policy import load_policy
@@ -115,16 +114,6 @@ def scan_command(
115114
try:
116115
_initialize_clients(ctx)
117116

118-
if tool == AIIDEType.CLAUDE_CODE:
119-
try:
120-
claude_config = load_claude_config()
121-
if claude_config:
122-
mcp_servers = get_mcp_servers(claude_config)
123-
ai_security_client = ctx.obj['ai_security_client']
124-
ai_security_client.report_mcp_servers(mcp_servers)
125-
except Exception as e:
126-
logger.debug('Failed to report MCP servers from Claude config', exc_info=e)
127-
128117
handler = get_handler_for_event(event_name)
129118
if handler is None:
130119
logger.debug('Unknown hook event, allowing by default', extra={'event_name': event_name})

cycode/cyclient/ai_security_manager_client.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ class AISecurityManagerClient:
1717

1818
_CONVERSATIONS_PATH = 'v4/ai-security/interactions/conversations'
1919
_EVENTS_PATH = 'v4/ai-security/interactions/events'
20-
_MCP_SERVERS_PATH = 'v4/ai-security/mcp-servers'
2120

2221
def __init__(self, client: CycodeClientBase, service_config: 'AISecurityManagerServiceConfigBase') -> None:
2322
self.client = client
@@ -55,22 +54,6 @@ def create_conversation(self, payload: 'AIHookPayload') -> Optional[str]:
5554

5655
return conversation_id
5756

58-
def report_mcp_servers(self, mcp_servers: list[dict]) -> None:
59-
"""Report MCP servers discovered from IDE config.
60-
61-
Posts the list of MCP server configurations to the backend.
62-
Failures are logged but do not block the hook flow.
63-
"""
64-
if not mcp_servers:
65-
return
66-
67-
body = {'mcp_servers': mcp_servers}
68-
69-
try:
70-
self.client.post(self._build_endpoint_path(self._MCP_SERVERS_PATH), body=body)
71-
except Exception as e:
72-
logger.debug('Failed to report MCP servers', exc_info=e)
73-
7457
def create_event(
7558
self,
7659
payload: 'AIHookPayload',

tests/cli/commands/ai_guardrails/scan/test_payload.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,60 @@ def test_from_claude_code_payload_gets_latest_user_uuid(mocker: MockerFixture) -
322322
assert unified.generation_id == 'latest-user-uuid'
323323

324324

325+
# Claude Code email extraction tests
326+
327+
328+
def test_from_claude_code_payload_extracts_email_from_config(mocker: MockerFixture) -> None:
329+
"""Test that ide_user_email is populated from ~/.claude.json."""
330+
mocker.patch(
331+
'cycode.cli.apps.ai_guardrails.scan.payload.load_claude_config',
332+
return_value={'oauthAccount': {'emailAddress': 'user@example.com'}},
333+
)
334+
335+
claude_payload = {
336+
'hook_event_name': 'UserPromptSubmit',
337+
'session_id': 'session-123',
338+
'prompt': 'test',
339+
}
340+
341+
unified = AIHookPayload.from_claude_code_payload(claude_payload)
342+
assert unified.ide_user_email == 'user@example.com'
343+
344+
345+
def test_from_claude_code_payload_email_none_when_config_missing(mocker: MockerFixture) -> None:
346+
"""Test that ide_user_email is None when ~/.claude.json is missing."""
347+
mocker.patch(
348+
'cycode.cli.apps.ai_guardrails.scan.payload.load_claude_config',
349+
return_value=None,
350+
)
351+
352+
claude_payload = {
353+
'hook_event_name': 'UserPromptSubmit',
354+
'session_id': 'session-123',
355+
'prompt': 'test',
356+
}
357+
358+
unified = AIHookPayload.from_claude_code_payload(claude_payload)
359+
assert unified.ide_user_email is None
360+
361+
362+
def test_from_claude_code_payload_email_none_when_no_oauth(mocker: MockerFixture) -> None:
363+
"""Test that ide_user_email is None when oauthAccount is missing from config."""
364+
mocker.patch(
365+
'cycode.cli.apps.ai_guardrails.scan.payload.load_claude_config',
366+
return_value={'someOtherKey': 'value'},
367+
)
368+
369+
claude_payload = {
370+
'hook_event_name': 'UserPromptSubmit',
371+
'session_id': 'session-123',
372+
'prompt': 'test',
373+
}
374+
375+
unified = AIHookPayload.from_claude_code_payload(claude_payload)
376+
assert unified.ide_user_email is None
377+
378+
325379
# IDE detection tests
326380

327381

tests/cli/commands/ai_guardrails/test_claude_config.py

Lines changed: 1 addition & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from pyfakefs.fake_filesystem import FakeFilesystem
77

8-
from cycode.cli.apps.ai_guardrails.scan.claude_config import get_mcp_servers, get_user_email, load_claude_config
8+
from cycode.cli.apps.ai_guardrails.scan.claude_config import get_user_email, load_claude_config
99

1010

1111
def test_load_claude_config_valid(fs: FakeFilesystem) -> None:
@@ -52,68 +52,3 @@ def test_get_user_email_missing_email_address() -> None:
5252
"""Test extracting email when oauthAccount exists but emailAddress is missing."""
5353
config = {'oauthAccount': {'someOtherField': 'value'}}
5454
assert get_user_email(config) is None
55-
56-
57-
def test_get_mcp_servers_stdio_and_http() -> None:
58-
"""Test extracting MCP servers with both stdio and http types."""
59-
config = {
60-
'mcpServers': {
61-
'gitlab': {
62-
'type': 'stdio',
63-
'command': '/opt/homebrew/bin/gitlab-mcp',
64-
'args': ['--verbose'],
65-
},
66-
'atlassian': {
67-
'type': 'http',
68-
'url': 'https://mcp.atlassian.com/v1/mcp',
69-
},
70-
},
71-
}
72-
73-
result = get_mcp_servers(config)
74-
assert len(result) == 2
75-
76-
gitlab = next(s for s in result if s['name'] == 'gitlab')
77-
assert gitlab['server_type'] == 'Local'
78-
assert gitlab['command'] == '/opt/homebrew/bin/gitlab-mcp'
79-
assert gitlab['args'] == ['--verbose']
80-
assert gitlab['url'] is None
81-
82-
atlassian = next(s for s in result if s['name'] == 'atlassian')
83-
assert atlassian['server_type'] == 'Remote'
84-
assert atlassian['command'] is None
85-
assert atlassian['url'] == 'https://mcp.atlassian.com/v1/mcp'
86-
87-
88-
def test_get_mcp_servers_empty() -> None:
89-
"""Test extracting MCP servers when mcpServers is empty."""
90-
config = {'mcpServers': {}}
91-
assert get_mcp_servers(config) == []
92-
93-
94-
def test_get_mcp_servers_missing_key() -> None:
95-
"""Test extracting MCP servers when mcpServers key is missing."""
96-
config = {'someOtherKey': 'value'}
97-
assert get_mcp_servers(config) == []
98-
99-
100-
def test_get_mcp_servers_invalid_type() -> None:
101-
"""Test extracting MCP servers when mcpServers is not a dict."""
102-
config = {'mcpServers': 'not a dict'}
103-
assert get_mcp_servers(config) == []
104-
105-
106-
def test_get_mcp_servers_unknown_server_type() -> None:
107-
"""Test that unknown server types are passed through as-is."""
108-
config = {
109-
'mcpServers': {
110-
'custom': {
111-
'type': 'sse',
112-
'url': 'https://example.com/sse',
113-
},
114-
},
115-
}
116-
117-
result = get_mcp_servers(config)
118-
assert len(result) == 1
119-
assert result[0]['server_type'] == 'sse'

0 commit comments

Comments
 (0)