mcpower-proxy 0.0.67__py3-none-any.whl → 0.0.77__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ide_tools/__init__.py +12 -0
- ide_tools/common/__init__.py +5 -0
- ide_tools/common/hooks/__init__.py +5 -0
- ide_tools/common/hooks/init.py +129 -0
- ide_tools/common/hooks/output.py +63 -0
- ide_tools/common/hooks/prompt_submit.py +136 -0
- ide_tools/common/hooks/read_file.py +170 -0
- ide_tools/common/hooks/shell_execution.py +257 -0
- ide_tools/common/hooks/shell_parser_bashlex.py +394 -0
- ide_tools/common/hooks/types.py +34 -0
- ide_tools/common/hooks/utils.py +286 -0
- ide_tools/cursor/__init__.py +11 -0
- ide_tools/cursor/constants.py +58 -0
- ide_tools/cursor/format.py +35 -0
- ide_tools/cursor/router.py +101 -0
- ide_tools/router.py +48 -0
- main.py +11 -4
- {mcpower_proxy-0.0.67.dist-info → mcpower_proxy-0.0.77.dist-info}/METADATA +4 -3
- mcpower_proxy-0.0.77.dist-info/RECORD +62 -0
- {mcpower_proxy-0.0.67.dist-info → mcpower_proxy-0.0.77.dist-info}/top_level.txt +1 -0
- modules/apis/security_policy.py +11 -6
- modules/decision_handler.py +219 -0
- modules/logs/audit_trail.py +20 -18
- modules/logs/logger.py +14 -18
- modules/redaction/gitleaks_rules.py +1 -1
- modules/redaction/pii_rules.py +0 -48
- modules/redaction/redactor.py +112 -107
- modules/ui/__init__.py +1 -1
- modules/ui/confirmation.py +0 -1
- modules/utils/cli.py +36 -6
- modules/utils/ids.py +50 -7
- modules/utils/json.py +3 -3
- modules/utils/platform.py +23 -0
- modules/utils/string.py +17 -0
- wrapper/__version__.py +1 -1
- wrapper/middleware.py +136 -221
- wrapper/server.py +19 -11
- mcpower_proxy-0.0.67.dist-info/RECORD +0 -43
- {mcpower_proxy-0.0.67.dist-info → mcpower_proxy-0.0.77.dist-info}/WHEEL +0 -0
- {mcpower_proxy-0.0.67.dist-info → mcpower_proxy-0.0.77.dist-info}/entry_points.txt +0 -0
- {mcpower_proxy-0.0.67.dist-info → mcpower_proxy-0.0.77.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common utilities for IDE hooks - IDE-agnostic
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from collections import Counter
|
|
8
|
+
from typing import Dict, Any, List, Callable, Optional
|
|
9
|
+
|
|
10
|
+
from mcpower_shared.mcp_types import create_policy_request, create_policy_response, AgentContext, EnvironmentContext, \
|
|
11
|
+
ServerRef, ToolRef
|
|
12
|
+
from modules.apis.security_policy import SecurityPolicyClient
|
|
13
|
+
from modules.decision_handler import DecisionHandler
|
|
14
|
+
from modules.logs.audit_trail import AuditTrailLogger
|
|
15
|
+
from modules.logs.logger import MCPLogger
|
|
16
|
+
from modules.redaction import redact
|
|
17
|
+
from modules.utils.json import safe_json_dumps
|
|
18
|
+
from wrapper.__version__ import __version__
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def create_validator(
|
|
22
|
+
required_fields: Dict[str, type],
|
|
23
|
+
optional_fields: Optional[Dict[str, type]] = None
|
|
24
|
+
) -> Callable[[str], Dict[str, Any]]:
|
|
25
|
+
"""
|
|
26
|
+
Factory for input validators
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
required_fields: Dict mapping field names to their expected types
|
|
30
|
+
optional_fields: Dict mapping optional field names to their expected types
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Validator function that parses and validates input
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def parse_and_validate_input(stdin_input: str) -> Dict[str, Any]:
|
|
37
|
+
try:
|
|
38
|
+
if not stdin_input.strip():
|
|
39
|
+
raise ValueError("No input provided")
|
|
40
|
+
input_data = json.loads(stdin_input)
|
|
41
|
+
except json.JSONDecodeError as e:
|
|
42
|
+
raise ValueError(f"Failed to parse input: {e}")
|
|
43
|
+
|
|
44
|
+
for field, expected_type in required_fields.items():
|
|
45
|
+
if field not in input_data:
|
|
46
|
+
raise ValueError(f"No {field} provided in input")
|
|
47
|
+
if not isinstance(input_data[field], expected_type):
|
|
48
|
+
raise ValueError(f"{field} must be a {expected_type.__name__}")
|
|
49
|
+
|
|
50
|
+
if optional_fields:
|
|
51
|
+
for field, expected_type in optional_fields.items():
|
|
52
|
+
if field in input_data and not isinstance(input_data[field], expected_type):
|
|
53
|
+
raise ValueError(f"{field} must be a {expected_type.__name__}")
|
|
54
|
+
|
|
55
|
+
return input_data
|
|
56
|
+
|
|
57
|
+
return parse_and_validate_input
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def extract_redaction_patterns(redacted_content: str) -> Dict[str, int]:
|
|
61
|
+
"""
|
|
62
|
+
Extract redaction pattern types and their counts from redacted content
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
redacted_content: Content with [REDACTED-type] placeholders
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dict mapping redaction types to counts
|
|
69
|
+
"""
|
|
70
|
+
pattern = r'\[REDACTED-([^\]]+)\]'
|
|
71
|
+
matches = re.findall(pattern, redacted_content)
|
|
72
|
+
return dict(Counter(matches))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build_sensitive_data_types(patterns: Dict[str, int], context: str = "file") -> Dict[str, Dict[str, Any]]:
|
|
76
|
+
"""
|
|
77
|
+
Convert redaction patterns to structured sensitive_data_types dict
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
patterns: Dict mapping pattern text to occurrence counts (from extract_redaction_patterns)
|
|
81
|
+
context: Context string for description (e.g., "file", "prompt text")
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Dict mapping data types to occurrence info with descriptions
|
|
85
|
+
"""
|
|
86
|
+
sensitive_data_types = {}
|
|
87
|
+
for pattern_text, count in patterns.items():
|
|
88
|
+
data_type = pattern_text.replace("[REDACTED-", "").replace("]", "")
|
|
89
|
+
sensitive_data_types[data_type] = {
|
|
90
|
+
"occurrences": count,
|
|
91
|
+
"description": f"Found {count} instance(s) of {data_type} in {context}"
|
|
92
|
+
}
|
|
93
|
+
return sensitive_data_types
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def process_single_file_for_redaction(
|
|
97
|
+
file_path: str,
|
|
98
|
+
content: str,
|
|
99
|
+
logger: MCPLogger
|
|
100
|
+
) -> Optional[Dict[str, Any]]:
|
|
101
|
+
"""
|
|
102
|
+
Process a single file's content for redaction patterns
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
file_path: Path to the file being processed
|
|
106
|
+
content: File content to check for redactions
|
|
107
|
+
logger: MCPLogger instance
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Dict with redaction info if sensitive data found, None otherwise
|
|
111
|
+
"""
|
|
112
|
+
redacted = redact(content)
|
|
113
|
+
patterns = extract_redaction_patterns(redacted)
|
|
114
|
+
if patterns:
|
|
115
|
+
sensitive_data_types = build_sensitive_data_types(patterns, "file")
|
|
116
|
+
logger.info(f"Found {len(patterns)} sensitive data type(s) in: {file_path}")
|
|
117
|
+
return {
|
|
118
|
+
"file_path": file_path,
|
|
119
|
+
"contains_sensitive_data": True,
|
|
120
|
+
"sensitive_data_types": sensitive_data_types,
|
|
121
|
+
"risk_summary": f"File contains {sum(patterns.values())} sensitive data item(s) across {len(patterns)} type(s)"
|
|
122
|
+
}
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def process_attachments_for_redaction(
|
|
127
|
+
attachments: List[Dict[str, Any]],
|
|
128
|
+
logger: MCPLogger
|
|
129
|
+
) -> List[Dict[str, Any]]:
|
|
130
|
+
"""
|
|
131
|
+
Process file attachments and extract redaction patterns
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
attachments: List of attachment dicts with 'type' and 'file_path' or 'filePath'
|
|
135
|
+
logger: MCPLogger instance
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of files with redactions found
|
|
139
|
+
"""
|
|
140
|
+
files_with_redactions = []
|
|
141
|
+
|
|
142
|
+
for attachment in attachments:
|
|
143
|
+
att_type = attachment.get("type")
|
|
144
|
+
att_path = attachment.get("file_path") or attachment.get("filePath")
|
|
145
|
+
|
|
146
|
+
if att_type != "file":
|
|
147
|
+
logger.debug(f"Skipping non-file attachment (type={att_type}): {att_path}")
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
if not att_path:
|
|
151
|
+
logger.debug("Skipping attachment with no file_path")
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
with open(att_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
156
|
+
content = f.read()
|
|
157
|
+
|
|
158
|
+
result = process_single_file_for_redaction(att_path, content, logger)
|
|
159
|
+
if result:
|
|
160
|
+
files_with_redactions.append(result)
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.warning(f"Could not read attachment file {att_path}: {e}")
|
|
164
|
+
|
|
165
|
+
return files_with_redactions
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def inspect_and_enforce(
|
|
169
|
+
is_request: bool,
|
|
170
|
+
session_id: str,
|
|
171
|
+
logger: MCPLogger,
|
|
172
|
+
audit_logger: AuditTrailLogger,
|
|
173
|
+
app_uid: str,
|
|
174
|
+
event_id: str,
|
|
175
|
+
server_name: str,
|
|
176
|
+
tool_name: str,
|
|
177
|
+
content_data: Dict[str, Any],
|
|
178
|
+
prompt_id: str,
|
|
179
|
+
cwd: Optional[str],
|
|
180
|
+
current_files: Optional[List[str]] = None,
|
|
181
|
+
client_name: str = "ide-tools"
|
|
182
|
+
) -> Dict[str, Any]:
|
|
183
|
+
"""
|
|
184
|
+
Generic handler for API inspection and decision enforcement
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
is_request: True for request inspection, False for response inspection
|
|
188
|
+
session_id: Session identifier
|
|
189
|
+
logger: Logger instance
|
|
190
|
+
audit_logger: Audit logger instance
|
|
191
|
+
app_uid: Application UID
|
|
192
|
+
event_id: Event identifier
|
|
193
|
+
server_name: Server name (IDE-specific, e.g. "cursor_tools_mcp")
|
|
194
|
+
tool_name: Tool/hook name
|
|
195
|
+
content_data: Data to inspect
|
|
196
|
+
prompt_id: Prompt identifier
|
|
197
|
+
cwd: Current working directory
|
|
198
|
+
current_files: Optional list of current files
|
|
199
|
+
client_name: Client name (e.g. "cursor", "claude-code")
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Decision dict from security API
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
Exception: If decision blocks the operation or API call fails
|
|
206
|
+
"""
|
|
207
|
+
agent_context = AgentContext(
|
|
208
|
+
last_user_prompt="",
|
|
209
|
+
user_prompt_id=prompt_id,
|
|
210
|
+
context_summary=""
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
env_context = EnvironmentContext(
|
|
214
|
+
session_id=session_id,
|
|
215
|
+
workspace={
|
|
216
|
+
"roots": [cwd] if cwd else [],
|
|
217
|
+
"current_files": current_files or []
|
|
218
|
+
},
|
|
219
|
+
client=client_name,
|
|
220
|
+
client_version=__version__,
|
|
221
|
+
selection_hash=""
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
async with SecurityPolicyClient(
|
|
225
|
+
session_id=session_id,
|
|
226
|
+
logger=logger,
|
|
227
|
+
audit_logger=audit_logger,
|
|
228
|
+
app_id=app_uid
|
|
229
|
+
) as client:
|
|
230
|
+
if is_request:
|
|
231
|
+
policy_request = create_policy_request(
|
|
232
|
+
event_id=event_id,
|
|
233
|
+
server=ServerRef(
|
|
234
|
+
name=server_name,
|
|
235
|
+
transport="stdio",
|
|
236
|
+
context="ide"
|
|
237
|
+
),
|
|
238
|
+
tool=ToolRef(
|
|
239
|
+
name=tool_name
|
|
240
|
+
),
|
|
241
|
+
agent_context=agent_context,
|
|
242
|
+
env_context=env_context,
|
|
243
|
+
arguments=content_data
|
|
244
|
+
)
|
|
245
|
+
decision = await client.inspect_policy_request(
|
|
246
|
+
policy_request=policy_request,
|
|
247
|
+
prompt_id=prompt_id
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
policy_response = create_policy_response(
|
|
251
|
+
event_id=event_id,
|
|
252
|
+
server=ServerRef(
|
|
253
|
+
name=server_name,
|
|
254
|
+
transport="stdio",
|
|
255
|
+
context="ide"
|
|
256
|
+
),
|
|
257
|
+
tool=ToolRef(
|
|
258
|
+
name=tool_name
|
|
259
|
+
),
|
|
260
|
+
response_content=safe_json_dumps(content_data),
|
|
261
|
+
agent_context=agent_context,
|
|
262
|
+
env_context=env_context
|
|
263
|
+
)
|
|
264
|
+
decision = await client.inspect_policy_response(
|
|
265
|
+
policy_response=policy_response,
|
|
266
|
+
prompt_id=prompt_id
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
await DecisionHandler(
|
|
270
|
+
logger=logger,
|
|
271
|
+
audit_logger=audit_logger,
|
|
272
|
+
session_id=session_id,
|
|
273
|
+
app_id=app_uid
|
|
274
|
+
).enforce_decision(
|
|
275
|
+
decision=decision,
|
|
276
|
+
is_request=is_request,
|
|
277
|
+
event_id=event_id,
|
|
278
|
+
tool_name=tool_name,
|
|
279
|
+
content_data=content_data,
|
|
280
|
+
operation_type="hook",
|
|
281
|
+
prompt_id=prompt_id,
|
|
282
|
+
server_name=server_name,
|
|
283
|
+
error_message_prefix=f"Operation blocked by security policy"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return decision
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cursor Hook Constants
|
|
3
|
+
|
|
4
|
+
Configuration values specific to Cursor hook handlers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
from ide_tools.common.hooks.types import HookConfig, OutputFormat
|
|
10
|
+
from ide_tools.cursor.format import cursor_output_formatter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HookPermission(str, Enum):
|
|
14
|
+
"""Cursor hook response permission values"""
|
|
15
|
+
ALLOW = "allow"
|
|
16
|
+
DENY = "deny"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Cursor-specific configuration
|
|
20
|
+
CURSOR_CONFIG = HookConfig(
|
|
21
|
+
output_format=OutputFormat(
|
|
22
|
+
allow_exit_code=0,
|
|
23
|
+
deny_exit_code=1,
|
|
24
|
+
error_exit_code=1,
|
|
25
|
+
formatter=cursor_output_formatter
|
|
26
|
+
),
|
|
27
|
+
server_name="mcpower_cursor",
|
|
28
|
+
client_name="cursor",
|
|
29
|
+
max_content_length=100000
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Hook descriptions from https://cursor.com/docs/agent/hooks#hook-events
|
|
33
|
+
CURSOR_HOOKS = {
|
|
34
|
+
"beforeShellExecution": {
|
|
35
|
+
"name": "beforeShellExecution",
|
|
36
|
+
"description": "Triggered before a shell command is executed by the agent. "
|
|
37
|
+
"Allows inspection and potential blocking of shell commands.",
|
|
38
|
+
"version": "1.0.0"
|
|
39
|
+
},
|
|
40
|
+
"afterShellExecution": {
|
|
41
|
+
"name": "afterShellExecution",
|
|
42
|
+
"description": "Triggered after a shell command completes execution. "
|
|
43
|
+
"Provides access to command output and exit status.",
|
|
44
|
+
"version": "1.0.0"
|
|
45
|
+
},
|
|
46
|
+
"beforeReadFile": {
|
|
47
|
+
"name": "beforeReadFile",
|
|
48
|
+
"description": "Triggered before the agent reads a file. "
|
|
49
|
+
"Allows inspection and potential blocking of file read operations.",
|
|
50
|
+
"version": "1.0.0"
|
|
51
|
+
},
|
|
52
|
+
"beforeSubmitPrompt": {
|
|
53
|
+
"name": "beforeSubmitPrompt",
|
|
54
|
+
"description": "Triggered before a prompt is submitted to the AI model. "
|
|
55
|
+
"Allows inspection and modification of prompts.",
|
|
56
|
+
"version": "1.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cursor-specific output formatting
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cursor_output_formatter(hook_type: str, allowed: bool, user_msg: Optional[str], agent_msg: Optional[str]) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Format output for Cursor IDE
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
hook_type: "permission" or "continue"
|
|
15
|
+
allowed: True for allow/continue, False for deny/block
|
|
16
|
+
user_msg: Message for user
|
|
17
|
+
agent_msg: Message for agent/logs
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
JSON string in Cursor format
|
|
21
|
+
"""
|
|
22
|
+
if hook_type == "permission":
|
|
23
|
+
result = {"permission": "allow" if allowed else "deny"}
|
|
24
|
+
if user_msg:
|
|
25
|
+
result["user_message"] = user_msg
|
|
26
|
+
if agent_msg:
|
|
27
|
+
result["agent_message"] = agent_msg
|
|
28
|
+
else: # continue
|
|
29
|
+
result = {"continue": allowed}
|
|
30
|
+
if user_msg:
|
|
31
|
+
result["user_message"] = user_msg
|
|
32
|
+
if agent_msg:
|
|
33
|
+
result["agent_message"] = agent_msg
|
|
34
|
+
|
|
35
|
+
return json.dumps(result)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cursor Router
|
|
3
|
+
|
|
4
|
+
Routes Cursor hook calls to appropriate handlers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
from ide_tools.common.hooks.init import handle_init
|
|
13
|
+
from ide_tools.common.hooks.prompt_submit import handle_prompt_submit
|
|
14
|
+
from ide_tools.common.hooks.read_file import handle_read_file
|
|
15
|
+
from ide_tools.common.hooks.shell_execution import handle_shell_execution
|
|
16
|
+
from modules.logs.audit_trail import AuditTrailLogger
|
|
17
|
+
from modules.logs.logger import MCPLogger
|
|
18
|
+
from .constants import CURSOR_HOOKS, CURSOR_CONFIG
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def route_cursor_hook(logger: MCPLogger, audit_logger: AuditTrailLogger, stdin_input: str):
|
|
22
|
+
"""
|
|
23
|
+
Route Cursor hook to appropriate shared handler
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
logger: MCPLogger instance
|
|
27
|
+
audit_logger: AuditTrailLogger instance
|
|
28
|
+
stdin_input: Raw input string from stdin
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
input_data = json.loads(stdin_input)
|
|
32
|
+
|
|
33
|
+
hook_event_name = input_data.get("hook_event_name")
|
|
34
|
+
if not hook_event_name:
|
|
35
|
+
logger.error("Missing required field 'hook_event_name' in input")
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
conversation_id = input_data.get("conversation_id")
|
|
39
|
+
if not conversation_id:
|
|
40
|
+
logger.error("Missing required field 'conversation_id' in input")
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
generation_id = input_data.get("generation_id")
|
|
44
|
+
if not generation_id:
|
|
45
|
+
logger.error("Missing required field 'generation_id' in input")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
workspace_roots = input_data.get("workspace_roots")
|
|
49
|
+
if workspace_roots is None:
|
|
50
|
+
logger.error("Missing required field 'workspace_roots' in input")
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
if not isinstance(workspace_roots, list):
|
|
54
|
+
logger.error("Invalid 'workspace_roots': must be a list")
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
prompt_id = conversation_id[:8]
|
|
58
|
+
event_id = uuid.uuid4().hex[:8]
|
|
59
|
+
cwd = workspace_roots[0] if workspace_roots else None
|
|
60
|
+
|
|
61
|
+
logger.info(
|
|
62
|
+
f"Cursor router: routing to {hook_event_name} handler "
|
|
63
|
+
f"(prompt_id={prompt_id}, event_id={event_id}, cwd={cwd})")
|
|
64
|
+
|
|
65
|
+
# Route to appropriate handler
|
|
66
|
+
if hook_event_name == "init":
|
|
67
|
+
asyncio.run(handle_init(
|
|
68
|
+
logger=logger,
|
|
69
|
+
audit_logger=audit_logger,
|
|
70
|
+
event_id=event_id,
|
|
71
|
+
prompt_id=prompt_id,
|
|
72
|
+
cwd=cwd,
|
|
73
|
+
server_name=CURSOR_CONFIG.server_name,
|
|
74
|
+
client_name="cursor",
|
|
75
|
+
hooks=CURSOR_HOOKS
|
|
76
|
+
))
|
|
77
|
+
elif hook_event_name == "beforeShellExecution":
|
|
78
|
+
asyncio.run(
|
|
79
|
+
handle_shell_execution(logger, audit_logger, stdin_input, prompt_id, event_id, cwd, CURSOR_CONFIG,
|
|
80
|
+
hook_event_name, is_request=True))
|
|
81
|
+
elif hook_event_name == "afterShellExecution":
|
|
82
|
+
asyncio.run(
|
|
83
|
+
handle_shell_execution(logger, audit_logger, stdin_input, prompt_id, event_id, cwd, CURSOR_CONFIG,
|
|
84
|
+
hook_event_name, is_request=False))
|
|
85
|
+
elif hook_event_name == "beforeReadFile":
|
|
86
|
+
asyncio.run(handle_read_file(logger, audit_logger, stdin_input, prompt_id, event_id, cwd, CURSOR_CONFIG,
|
|
87
|
+
hook_event_name))
|
|
88
|
+
elif hook_event_name == "beforeSubmitPrompt":
|
|
89
|
+
asyncio.run(
|
|
90
|
+
handle_prompt_submit(logger, audit_logger, stdin_input, prompt_id, event_id, cwd, CURSOR_CONFIG,
|
|
91
|
+
hook_event_name))
|
|
92
|
+
else:
|
|
93
|
+
logger.error(f"Unknown hook_event_name: {hook_event_name}")
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
except json.JSONDecodeError as e:
|
|
97
|
+
logger.error(f"Failed to parse input JSON: {e}")
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Routing error: {e}", exc_info=True)
|
|
101
|
+
sys.exit(1)
|
ide_tools/router.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IDE Tools Router
|
|
3
|
+
|
|
4
|
+
Routes hook calls to appropriate IDE-specific routers based on --ide flag.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from modules.logs.audit_trail import AuditTrailLogger
|
|
11
|
+
from modules.logs.logger import MCPLogger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(logger: MCPLogger, audit_logger: AuditTrailLogger, ide: str, context: Optional[str]):
|
|
15
|
+
"""
|
|
16
|
+
Main entry point for IDE tools - routes to appropriate IDE router
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
logger: MCPLogger instance
|
|
20
|
+
audit_logger: AuditTrailLogger instance
|
|
21
|
+
ide: IDE name (e.g., "cursor")
|
|
22
|
+
context: Additional context (to be verified as optional by the associated --ide handler)
|
|
23
|
+
"""
|
|
24
|
+
# Monkey patch sys.exit to log exit code
|
|
25
|
+
original_exit = sys.exit
|
|
26
|
+
|
|
27
|
+
def logged_exit(code=0):
|
|
28
|
+
logger.info(f"sys.exit called with code: {code}")
|
|
29
|
+
original_exit(code)
|
|
30
|
+
|
|
31
|
+
sys.exit = logged_exit
|
|
32
|
+
|
|
33
|
+
logger.info(f"IDE Tools router: ide={ide}, context={context}")
|
|
34
|
+
|
|
35
|
+
# Read stdin input once at the top level (raw string)
|
|
36
|
+
# Each handler will parse it according to its own schema
|
|
37
|
+
stdin_input = sys.stdin.read()
|
|
38
|
+
|
|
39
|
+
# Route to appropriate IDE handler with the raw input string
|
|
40
|
+
if ide == "cursor":
|
|
41
|
+
from ide_tools.cursor import route_cursor_hook
|
|
42
|
+
route_cursor_hook(logger, audit_logger, stdin_input)
|
|
43
|
+
elif ide == "claude-code":
|
|
44
|
+
from ide_tools.claude_code import route_claude_code_hook
|
|
45
|
+
route_claude_code_hook(logger, audit_logger, stdin_input)
|
|
46
|
+
else:
|
|
47
|
+
logger.error(f"Unknown IDE: {ide}")
|
|
48
|
+
sys.exit(1)
|
main.py
CHANGED
|
@@ -45,13 +45,20 @@ def main():
|
|
|
45
45
|
logger.info('=' * 66)
|
|
46
46
|
logger.info('')
|
|
47
47
|
|
|
48
|
-
# Start config monitoring
|
|
49
|
-
config.start_monitoring(logger)
|
|
50
|
-
|
|
51
48
|
# Setup audit trail logging
|
|
52
49
|
audit_logger = setup_audit_trail_logger(logger)
|
|
53
50
|
|
|
54
|
-
|
|
51
|
+
if args.ide_tool:
|
|
52
|
+
from ide_tools.router import main as ide_tools_main
|
|
53
|
+
ide_tools_main(logger, audit_logger, args.ide, args.context)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
# Continue with MCP wrapper mode
|
|
57
|
+
# Start config monitoring
|
|
58
|
+
config.start_monitoring(logger)
|
|
59
|
+
|
|
60
|
+
logger.info(
|
|
61
|
+
f"Starting MCPower Proxy:\n{{'args': {args}, 'log_file': {log_file}, 'log_level': {log_level}, 'debug_mode': {debug_mode}}}")
|
|
55
62
|
|
|
56
63
|
try:
|
|
57
64
|
# Parse JSON/JSONC config
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcpower-proxy
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.77
|
|
4
4
|
Summary: MCPower Security proxy
|
|
5
5
|
Author-email: MCPower Security <support@mcpower.tech>
|
|
6
6
|
License: Apache License
|
|
@@ -209,14 +209,15 @@ Keywords: mcp,security,proxy,monitoring,audit,redaction,policy-enforcement
|
|
|
209
209
|
Requires-Python: ~=3.11.0
|
|
210
210
|
Description-Content-Type: text/markdown
|
|
211
211
|
License-File: LICENSE
|
|
212
|
-
Requires-Dist: fastmcp==2.13.0.
|
|
212
|
+
Requires-Dist: fastmcp==2.13.0.2
|
|
213
213
|
Requires-Dist: httpx>=0.25.0
|
|
214
214
|
Requires-Dist: mcp>=1.0.0
|
|
215
215
|
Requires-Dist: watchdog>=3.0.0
|
|
216
216
|
Requires-Dist: jsonc-parser>=1.1.5
|
|
217
217
|
Requires-Dist: jsonpath-ng>=1.7.0
|
|
218
218
|
Requires-Dist: pydantic>=2.8.0
|
|
219
|
-
Requires-Dist: mcpower-shared==0.1.
|
|
219
|
+
Requires-Dist: mcpower-shared==0.1.4
|
|
220
|
+
Requires-Dist: bashlex>=0.18
|
|
220
221
|
Dynamic: license-file
|
|
221
222
|
|
|
222
223
|
# MCPower Proxy
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
main.py,sha256=MahoqxG5euEJwLhosv2OVUkVbTaMkph_je8lgmtc7Z4,3920
|
|
2
|
+
ide_tools/__init__.py,sha256=odLisqEeKVuZupQoeM-uP9WgQseVTYQG0eP2hhwUyAc,250
|
|
3
|
+
ide_tools/router.py,sha256=PbGz1cwUXR3Bcy879W2wgmT4f2uThXiXevpWSxcgKZQ,1538
|
|
4
|
+
ide_tools/common/__init__.py,sha256=cW2cN5y2l4PHGkkWtYgydrtXtmLU_UipJAf_c0VMTk0,65
|
|
5
|
+
ide_tools/common/hooks/__init__.py,sha256=O-waaJU1OQFxplrLZymSSiIyLDNlGlki6nX-nbJiqz0,61
|
|
6
|
+
ide_tools/common/hooks/init.py,sha256=X0J_rEjU6qjvDaPOh-obzSqfICMsOGc6VxRjcZqLTqQ,4117
|
|
7
|
+
ide_tools/common/hooks/output.py,sha256=FBezJL1S5vzLJWW_cLht4XFLDJvhV_w-UTqCQOLEusE,1879
|
|
8
|
+
ide_tools/common/hooks/prompt_submit.py,sha256=_FysGrlJXcblex-OapWVIv1bedP1IKR2d1cvkINU0uM,4907
|
|
9
|
+
ide_tools/common/hooks/read_file.py,sha256=FBHTxOXrQWlw2qtddAq4gfv5h1g6rTbQ-0uqqL9wXE0,6456
|
|
10
|
+
ide_tools/common/hooks/shell_execution.py,sha256=KbJSGLW__bPso45eAlbV13nnkKYwwd7-KodZ6VyUnS0,9522
|
|
11
|
+
ide_tools/common/hooks/shell_parser_bashlex.py,sha256=ePit2jduHvBYkmEvcslpjCGJ5H-amVEm1We1vVCmMNc,15002
|
|
12
|
+
ide_tools/common/hooks/types.py,sha256=ndisZ8YoasQ3HlVzzyLmpiBCQJs43YPmixbaIIxRpZM,1023
|
|
13
|
+
ide_tools/common/hooks/utils.py,sha256=7Q95IZZG_lShq5Jvw78M7QN5-tVNVeFzLU33qB8boEU,9425
|
|
14
|
+
ide_tools/cursor/__init__.py,sha256=YW_V8m0A0bou0wQW_wy3nt2L_7MaNWeNKYBx-NQkilw,153
|
|
15
|
+
ide_tools/cursor/constants.py,sha256=KMRBRqxRxTdemMoq81TVG6avt3ATtsPo4aJo8XhSctk,1804
|
|
16
|
+
ide_tools/cursor/format.py,sha256=Lh-KH1IlsLL-0B98Bz-ywxpwXL8urK7yPteZGBTPOiY,972
|
|
17
|
+
ide_tools/cursor/router.py,sha256=GXmXcNSIA9pe1vnmmoYpz0dyC_B7OSpQwOVHIV7YUEI,3893
|
|
18
|
+
mcpower_proxy-0.0.77.dist-info/licenses/LICENSE,sha256=U6WUzdnBrbmVxBmY75ikW-KtinwYnowZ7yNb5hECrvY,11337
|
|
19
|
+
modules/__init__.py,sha256=mJglXQwSRhU-bBv4LXgfu7NfGN9K4BeQWMPApen5rAA,30
|
|
20
|
+
modules/decision_handler.py,sha256=P8isKzf4GIWz9SK-VJPtO8VJEgNp7rAIcVZngnaLHmw,9574
|
|
21
|
+
modules/apis/__init__.py,sha256=Y5WZpKJzHpnRJebk0F80ZRTjR2PpA2LlYLgqI3XlmRo,15
|
|
22
|
+
modules/apis/security_policy.py,sha256=3fljaTpzfh_Nj0-jVvbnCJBL-CZxvqFNWfSlrAawsBc,15103
|
|
23
|
+
modules/logs/__init__.py,sha256=dpboUQjuO02z8K-liCbm2DYkCa-CB_ZDV9WSSjNm7Fs,15
|
|
24
|
+
modules/logs/audit_trail.py,sha256=yoxzvRDI8ldjC-5o9__PkvaAbM33jWbq_8Sm6uKj8o8,6198
|
|
25
|
+
modules/logs/logger.py,sha256=MJS0P8VEzUX-5udzQitznaBPCBAcZJCygUgwaDWSq94,4087
|
|
26
|
+
modules/redaction/__init__.py,sha256=e5NTmp-zonUdzzscih-w_WQ-X8Nvb8CE8b_d6SbrwWg,316
|
|
27
|
+
modules/redaction/constants.py,sha256=xbDSX8n72FuJu6JJ_sbBE0f5OcWuwEwHxBZuK9Xz-TI,1213
|
|
28
|
+
modules/redaction/gitleaks_rules.py,sha256=8NHR5kr2XSrPn4c0PsjD0je6p8gmNzq2vI098QUxAx0,46397
|
|
29
|
+
modules/redaction/pii_rules.py,sha256=H7WOIMZZK8mkwdvnBe0J2nPwqzKqGHdUbCSGpxJGT8s,7666
|
|
30
|
+
modules/redaction/redactor.py,sha256=Y3PSxXJSLJZj8gkKZ3OQ7XxoIPUilFk6gY0dbeqfjwE,23352
|
|
31
|
+
modules/ui/__init__.py,sha256=YlW4XfQEGW1ezg3UFU59nHw95LicmlpNPhim5IqSB50,34
|
|
32
|
+
modules/ui/classes.py,sha256=ZvVRdzO_hD4WnpS3_eVa0WCyaooXiYVpHLzQkzBaH6M,1777
|
|
33
|
+
modules/ui/confirmation.py,sha256=y2A8j5_z64cDdbvUEh4lI8iTgTFBcTjVbIXioMfmkpw,7740
|
|
34
|
+
modules/ui/simple_dialog.py,sha256=PZW3WSPUVtnGXx-Kkg6hTQTr5NvpTQVhgHyro1z_3aY,3900
|
|
35
|
+
modules/ui/xdialog/__init__.py,sha256=KYQKVF6pGrwc99swRBxtWVXM__j9kVX_r6KikzbCOM4,9359
|
|
36
|
+
modules/ui/xdialog/constants.py,sha256=UjtqzT_O3OHUXJOyeTGroOUnaxdVyYukf7kK6vj1rog,200
|
|
37
|
+
modules/ui/xdialog/mac_dialogs.py,sha256=6r3hkJzJJdHSt-aH1Hy4lZ1MEuZK4Kc5D_YiWglKHAA,6129
|
|
38
|
+
modules/ui/xdialog/tk_dialogs.py,sha256=isxxN_mvZUFUQu8RD1J-GC7UMH2spqR3v_domgRbczQ,2403
|
|
39
|
+
modules/ui/xdialog/windows_custom_dialog.py,sha256=tcdo35d4ZoBydAj-4yzzgW2luw97-Sdjsr3X_3-a7jM,14849
|
|
40
|
+
modules/ui/xdialog/windows_dialogs.py,sha256=ohOoK4ciyv2s4BC9r7-zvGL6mECM-RCPTVOmzDnD6VQ,7626
|
|
41
|
+
modules/ui/xdialog/windows_structs.py,sha256=xzG44OGT5hBFnimJgOLXZBhmpQ_9CFxjtz-QNjP-VCw,8698
|
|
42
|
+
modules/ui/xdialog/yad_dialogs.py,sha256=EiajZVJg-xDwYymz1fyQwLtT5DzbJR3e8plMEnOgcpo,6933
|
|
43
|
+
modules/ui/xdialog/zenity_dialogs.py,sha256=wE71I_Ovf0sjhxHVNocbrhhDd8Y8X8loLETp8TMGMPQ,4512
|
|
44
|
+
modules/utils/__init__.py,sha256=Ptwu1epT_dW6EHjGkzGHAB-MbrrmYAlcPXGGcr4PvwE,20
|
|
45
|
+
modules/utils/cli.py,sha256=5QLwsZNMKdyh3xt4NDk20vCGD-eqZ514LnVWA0J0Lbg,2414
|
|
46
|
+
modules/utils/config.py,sha256=YuGrIYfBsOYABWjFoZosObPz-R7Wdul16RnDed_glYI,6654
|
|
47
|
+
modules/utils/copy.py,sha256=9OJIqWn8PxPZXr3DTt_01jp0YgmPimckab1969WFh0c,1075
|
|
48
|
+
modules/utils/ids.py,sha256=rhhRz7RmFjuJGYLft1erHz7vJII1DRpr3iHxBhhFl1s,5743
|
|
49
|
+
modules/utils/json.py,sha256=OA-JtSBqh9qd1yfm-iyOefNBMH3ITFUdxAkj7O_JZ-Y,4024
|
|
50
|
+
modules/utils/mcp_configs.py,sha256=DZaujZnF9LlPDJHzyepH7fWSt1GTr-FEmShPCqnZ5aI,1829
|
|
51
|
+
modules/utils/platform.py,sha256=Kz1Dh_UkvMbfXqiyZIEj7INYaWE9wT6nM9WIKXM91uM,552
|
|
52
|
+
modules/utils/string.py,sha256=cOuwWReyBwOgjxtTRPq1R6pCpfkgfqVmjdiaDPufIZU,456
|
|
53
|
+
wrapper/__init__.py,sha256=OJUsuWSoN1JqIHq4bSrzuL7ufcYJcwAmYCrJjLH44LM,22
|
|
54
|
+
wrapper/__version__.py,sha256=4LwZ6ftjxW5rGtS8e8B6V7iXNQZbgnact3whK05F-gg,82
|
|
55
|
+
wrapper/middleware.py,sha256=rXOIduymDLeJ56AWmpMnRnkf7sCmSz7jADMk-v8lcsw,30232
|
|
56
|
+
wrapper/schema.py,sha256=O-CtKI9eJ4eEnqeUXPCrK7QJAFJrdp_cFbmMyg452Aw,7952
|
|
57
|
+
wrapper/server.py,sha256=zoIW_bqXV9vKZNFtD-ij0X_LtKJEjucda6lQI5mU6qY,3440
|
|
58
|
+
mcpower_proxy-0.0.77.dist-info/METADATA,sha256=qX1CZtp76YNug7gOFHOmdJG4gXn3FvV7P_bHQ9iySV0,15698
|
|
59
|
+
mcpower_proxy-0.0.77.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
60
|
+
mcpower_proxy-0.0.77.dist-info/entry_points.txt,sha256=0smL8dxE7ERNz6XEggNaUC3QzKp8mD-v4q5nVEo0MXE,48
|
|
61
|
+
mcpower_proxy-0.0.77.dist-info/top_level.txt,sha256=OcCYMHHqbZq3mP5IZDRGdHUNOsKhT1XVnk_mDF_49Es,31
|
|
62
|
+
mcpower_proxy-0.0.77.dist-info/RECORD,,
|