mcpower-proxy 0.0.67__py3-none-any.whl → 0.0.73__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.
Files changed (36) hide show
  1. ide_tools/__init__.py +12 -0
  2. ide_tools/common/__init__.py +6 -0
  3. ide_tools/common/hooks/__init__.py +6 -0
  4. ide_tools/common/hooks/init.py +125 -0
  5. ide_tools/common/hooks/output.py +64 -0
  6. ide_tools/common/hooks/prompt_submit.py +186 -0
  7. ide_tools/common/hooks/read_file.py +170 -0
  8. ide_tools/common/hooks/shell_execution.py +196 -0
  9. ide_tools/common/hooks/types.py +35 -0
  10. ide_tools/common/hooks/utils.py +276 -0
  11. ide_tools/cursor/__init__.py +11 -0
  12. ide_tools/cursor/constants.py +58 -0
  13. ide_tools/cursor/format.py +35 -0
  14. ide_tools/cursor/router.py +100 -0
  15. ide_tools/router.py +48 -0
  16. main.py +11 -4
  17. {mcpower_proxy-0.0.67.dist-info → mcpower_proxy-0.0.73.dist-info}/METADATA +3 -3
  18. mcpower_proxy-0.0.73.dist-info/RECORD +59 -0
  19. {mcpower_proxy-0.0.67.dist-info → mcpower_proxy-0.0.73.dist-info}/top_level.txt +1 -0
  20. modules/apis/security_policy.py +11 -6
  21. modules/decision_handler.py +219 -0
  22. modules/logs/audit_trail.py +16 -15
  23. modules/logs/logger.py +14 -18
  24. modules/redaction/redactor.py +112 -107
  25. modules/ui/__init__.py +1 -1
  26. modules/ui/confirmation.py +0 -1
  27. modules/utils/cli.py +36 -6
  28. modules/utils/ids.py +50 -7
  29. modules/utils/json.py +3 -3
  30. wrapper/__version__.py +1 -1
  31. wrapper/middleware.py +115 -212
  32. wrapper/server.py +19 -11
  33. mcpower_proxy-0.0.67.dist-info/RECORD +0 -43
  34. {mcpower_proxy-0.0.67.dist-info → mcpower_proxy-0.0.73.dist-info}/WHEEL +0 -0
  35. {mcpower_proxy-0.0.67.dist-info → mcpower_proxy-0.0.73.dist-info}/entry_points.txt +0 -0
  36. {mcpower_proxy-0.0.67.dist-info → mcpower_proxy-0.0.73.dist-info}/licenses/LICENSE +0 -0
ide_tools/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """
2
+ IDE Tools - IDE Safety Validation Module
3
+
4
+ Provides validation and user confirmation for AI-generated operations in IDEs.
5
+ Supports multiple IDEs with per-IDE handlers.
6
+ """
7
+
8
+ from .router import main as router_main
9
+
10
+ __all__ = [
11
+ "router_main",
12
+ ]
@@ -0,0 +1,6 @@
1
+ """
2
+ Common IDE Tools
3
+
4
+ Shared logic for all IDE integrations.
5
+ """
6
+
@@ -0,0 +1,6 @@
1
+ """
2
+ Common Hook Handlers
3
+
4
+ Shared hook logic for all IDEs
5
+ """
6
+
@@ -0,0 +1,125 @@
1
+ """
2
+ Shared initialization logic for IDE tools
3
+
4
+ Registers IDE hooks with the security API.
5
+ """
6
+
7
+ import sys
8
+ from typing import Dict, Optional
9
+
10
+ from mcpower_shared.mcp_types import InitRequest, EnvironmentContext, ServerRef, ToolRef
11
+ from modules.apis.security_policy import SecurityPolicyClient
12
+ from modules.logs.audit_trail import AuditTrailLogger
13
+ from modules.logs.logger import MCPLogger
14
+ from modules.utils.ids import get_session_id, read_app_uid, get_project_mcpower_dir
15
+ from modules.utils.json import safe_json_dumps
16
+ from wrapper.__version__ import __version__
17
+
18
+
19
+ def output_init_result(success: bool, message: str):
20
+ """
21
+ Output init result to stdout
22
+
23
+ Args:
24
+ success: True if initialization succeeded
25
+ message: Status message
26
+ """
27
+ result = {
28
+ "success": success,
29
+ "message": message
30
+ }
31
+
32
+ print(safe_json_dumps(result), flush=True)
33
+
34
+
35
+ async def handle_init(
36
+ logger: MCPLogger,
37
+ audit_logger: AuditTrailLogger,
38
+ event_id: str,
39
+ cwd: Optional[str],
40
+ server_name: str,
41
+ client_name: str,
42
+ hooks: Dict[str, Dict[str, str]]
43
+ ) -> None:
44
+ """
45
+ Generic init handler - registers hooks with security API
46
+
47
+ Args:
48
+ logger: Logger instance
49
+ audit_logger: Audit logger instance
50
+ event_id: Event identifier
51
+ cwd: Current working directory
52
+ server_name: IDE-specific server name (e.g. "cursor_tools_mcp")
53
+ client_name: IDE-specific client name (e.g. "cursor", "claude-code")
54
+ hooks: Dict of hook definitions with {name, description, version}
55
+
56
+ Outputs result and exits with appropriate code.
57
+ """
58
+ session_id = get_session_id()
59
+
60
+ logger.info(f"Init handler started (client={client_name}, event_id={event_id}, cwd={cwd})")
61
+
62
+ try:
63
+ app_uid = read_app_uid(logger, get_project_mcpower_dir(cwd))
64
+ audit_logger.set_app_uid(app_uid)
65
+
66
+ audit_logger.log_event("mcpower_start", {
67
+ "wrapper_version": __version__,
68
+ "wrapped_server_name": server_name,
69
+ "client": client_name
70
+ })
71
+
72
+ try:
73
+ tools = [
74
+ ToolRef(
75
+ name=hook_info["name"],
76
+ description=hook_info["description"],
77
+ version=hook_info["version"]
78
+ )
79
+ for hook_info in hooks.values()
80
+ ]
81
+
82
+ init_request = InitRequest(
83
+ environment=EnvironmentContext(
84
+ session_id=session_id,
85
+ workspace={
86
+ "roots": [cwd] if cwd else [],
87
+ "current_files": []
88
+ },
89
+ client=client_name,
90
+ client_version=__version__,
91
+ selection_hash=""
92
+ ),
93
+ server=ServerRef(
94
+ name=server_name,
95
+ transport="stdio",
96
+ version="1.0.0",
97
+ context="ide"
98
+ ),
99
+ tools=tools
100
+ )
101
+
102
+ async with SecurityPolicyClient(
103
+ session_id=session_id,
104
+ logger=logger,
105
+ audit_logger=audit_logger,
106
+ app_id=app_uid
107
+ ) as client:
108
+ await client.init_tools(init_request, event_id=event_id)
109
+
110
+ logger.info(f"Hooks registered successfully for {client_name}")
111
+
112
+ # Success - output result and exit
113
+ output_init_result(True, f"{client_name.title()} hooks registered successfully")
114
+ sys.exit(0)
115
+
116
+ except Exception as e:
117
+ logger.error(f"API initialization failed: {e}")
118
+ output_init_result(False, f"Error: {str(e)}")
119
+ sys.exit(1)
120
+
121
+ except Exception as e:
122
+ logger.error(f"Unexpected error in init handler: {e}", exc_info=True)
123
+ output_init_result(False, f"Initialization failed: {str(e)}")
124
+ sys.exit(1)
125
+
@@ -0,0 +1,64 @@
1
+ """
2
+ IDE-agnostic output handling
3
+ """
4
+
5
+ import sys
6
+ from typing import Optional
7
+
8
+ from modules.logs.logger import MCPLogger
9
+ from .types import OutputFormat
10
+
11
+
12
+ def output_result(
13
+ logger: MCPLogger,
14
+ output_format: OutputFormat,
15
+ hook_type: str,
16
+ allowed: bool,
17
+ user_message: Optional[str] = None,
18
+ agent_message: Optional[str] = None
19
+ ) -> None:
20
+ """
21
+ Output hook result in IDE-specific format and exit with appropriate code
22
+
23
+ Args:
24
+ logger: Logger instance
25
+ output_format: IDE-specific output configuration
26
+ hook_type: "permission" or "continue"
27
+ allowed: True for allow/continue, False for deny/block
28
+ user_message: Optional message for user
29
+ agent_message: Optional message for agent/logs
30
+ """
31
+ # Format output using IDE-specific formatter
32
+ formatted_output = output_format.formatter(hook_type, allowed, user_message, agent_message)
33
+
34
+ logger.info(f"Hook output ({hook_type}, allowed={allowed}): {formatted_output}")
35
+ print(formatted_output, flush=True)
36
+
37
+ # Exit with appropriate code
38
+ exit_code = output_format.allow_exit_code if allowed else output_format.deny_exit_code
39
+ sys.exit(exit_code)
40
+
41
+
42
+ def output_error(
43
+ logger: MCPLogger,
44
+ output_format: OutputFormat,
45
+ hook_type: str,
46
+ error_message: str
47
+ ) -> None:
48
+ """
49
+ Output error and exit with error code
50
+
51
+ Args:
52
+ logger: Logger instance
53
+ output_format: IDE-specific output configuration
54
+ hook_type: "permission" or "continue"
55
+ error_message: Error message
56
+ """
57
+ logger.error(f"Hook error: {error_message}")
58
+
59
+ # Output as deny/block with error message
60
+ formatted_output = output_format.formatter(hook_type, False, error_message, error_message)
61
+ print(formatted_output, flush=True)
62
+
63
+ sys.exit(output_format.error_exit_code)
64
+
@@ -0,0 +1,186 @@
1
+ """
2
+ Shared logic for UserPromptSubmit hook
3
+ """
4
+
5
+ from typing import Dict, Any, Optional
6
+
7
+ from modules.logs.audit_trail import AuditTrailLogger
8
+ from modules.logs.logger import MCPLogger
9
+ from modules.redaction import redact
10
+ from modules.utils.ids import get_session_id, read_app_uid, get_project_mcpower_dir
11
+ from .output import output_result, output_error
12
+ from .types import HookConfig
13
+ from .utils import create_validator, extract_redaction_patterns, build_sensitive_data_types, \
14
+ process_attachments_for_redaction, inspect_and_enforce
15
+
16
+
17
+ async def handle_prompt_submit(
18
+ logger: MCPLogger,
19
+ audit_logger: AuditTrailLogger,
20
+ stdin_input: str,
21
+ prompt_id: str,
22
+ event_id: str,
23
+ cwd: Optional[str],
24
+ config: HookConfig,
25
+ tool_name: str
26
+ ) -> None:
27
+ """
28
+ Shared handler for prompt submission hooks
29
+
30
+ Args:
31
+ logger: Logger instance
32
+ audit_logger: Audit logger instance
33
+ stdin_input: Raw JSON input
34
+ prompt_id: Prompt/conversation ID
35
+ event_id: Event/generation ID
36
+ cwd: Current working directory
37
+ config: IDE-specific hook configuration
38
+ tool_name: IDE-specific tool name (e.g., "beforeSubmitPrompt", "UserPromptSubmit")
39
+ """
40
+ session_id = get_session_id()
41
+ logger.info(
42
+ f"Prompt submit handler started (client={config.client_name}, prompt_id={prompt_id}, "
43
+ f"event_id={event_id}, cwd={cwd})")
44
+
45
+ app_uid = read_app_uid(logger, get_project_mcpower_dir(cwd))
46
+ audit_logger.set_app_uid(app_uid)
47
+
48
+ try:
49
+ # Validate input
50
+ try:
51
+ validator = create_validator(
52
+ required_fields={"prompt": str},
53
+ optional_fields={"attachments": list}
54
+ )
55
+ input_data = validator(stdin_input)
56
+ prompt = input_data["prompt"]
57
+ attachments = input_data.get("attachments", [])
58
+ except ValueError as e:
59
+ logger.error(f"Input validation error: {e}")
60
+ output_error(logger, config.output_format, "continue", str(e))
61
+ return
62
+
63
+ # Check for redactions in prompt
64
+ redacted_prompt = redact(prompt)
65
+ # Log audit event
66
+ audit_logger.log_event(
67
+ "prompt_submission",
68
+ {
69
+ "server": config.server_name,
70
+ "tool": tool_name,
71
+ "params": {"prompt": f"{redacted_prompt[:20]}...", "attachments_count": len(attachments)}
72
+ },
73
+ event_id=event_id
74
+ )
75
+
76
+ prompt_patterns = extract_redaction_patterns(redacted_prompt)
77
+
78
+ # Check for redactions in file attachments
79
+ files_with_redactions = process_attachments_for_redaction(
80
+ attachments,
81
+ logger
82
+ )
83
+
84
+ has_any_redactions = bool(prompt_patterns) or len(files_with_redactions) > 0
85
+
86
+ # If no redactions found, allow immediately without API call
87
+ if not has_any_redactions:
88
+ logger.info("No sensitive data found in prompt or attachments - allowing without API call")
89
+
90
+ audit_logger.log_event(
91
+ "prompt_submission_forwarded",
92
+ {
93
+ "server": config.server_name,
94
+ "tool": tool_name,
95
+ "params": {"redactions_found": has_any_redactions}
96
+ },
97
+ event_id=event_id
98
+ )
99
+
100
+ output_result(logger, config.output_format, "continue", True)
101
+ return
102
+
103
+ logger.info(f"Found redactions in prompt or {len(files_with_redactions)} file(s) - calling API for inspection")
104
+
105
+ # Build explicit content_data structure showing security risk
106
+ content_data: Dict[str, Any] = {
107
+ "security_alert": "Sensitive data detected in user prompt submission"
108
+ }
109
+
110
+ # Add prompt analysis if sensitive data found in prompt text
111
+ if prompt_patterns:
112
+ sensitive_data_types = build_sensitive_data_types(prompt_patterns, "prompt text")
113
+
114
+ total_prompt_items = sum(prompt_patterns.values())
115
+ content_data["user_prompt_analysis"] = {
116
+ "contains_sensitive_data": True,
117
+ "sensitive_data_types": sensitive_data_types,
118
+ "risk_summary": f"Prompt contains {total_prompt_items} sensitive data item(s) across {len(prompt_patterns)} type(s)"
119
+ }
120
+
121
+ # Add file analysis if sensitive data found in attachments
122
+ if files_with_redactions:
123
+ total_file_items = sum(
124
+ sum(f["sensitive_data_types"][dt]["occurrences"] for dt in f["sensitive_data_types"])
125
+ for f in files_with_redactions
126
+ )
127
+ content_data["attached_files_with_secrets_or_pii"] = files_with_redactions
128
+ content_data["files_summary"] = \
129
+ f"{len(files_with_redactions)} file(s) contain {total_file_items} sensitive data item(s)"
130
+
131
+ # Calculate overall risk level
132
+ total_sensitive_items = sum(prompt_patterns.values()) if prompt_patterns else 0
133
+ if files_with_redactions:
134
+ total_sensitive_items += sum(
135
+ sum(f["sensitive_data_types"][dt]["occurrences"] for dt in f["sensitive_data_types"])
136
+ for f in files_with_redactions
137
+ )
138
+ content_data["overall_summary"] = f"Total: {total_sensitive_items} sensitive data item(s) detected"
139
+
140
+ # Call security API and enforce decision
141
+ try:
142
+ decision = await inspect_and_enforce(
143
+ is_request=True,
144
+ session_id=session_id,
145
+ logger=logger,
146
+ audit_logger=audit_logger,
147
+ app_uid=app_uid,
148
+ event_id=event_id,
149
+ server_name=config.server_name,
150
+ tool_name=tool_name,
151
+ content_data=content_data,
152
+ prompt_id=prompt_id,
153
+ cwd=cwd,
154
+ client_name=config.client_name
155
+ )
156
+
157
+ # Log audit event for forwarding
158
+ audit_logger.log_event(
159
+ "prompt_submission_forwarded",
160
+ {
161
+ "server": config.server_name,
162
+ "tool": tool_name,
163
+ "params": {"redactions_found": has_any_redactions}
164
+ },
165
+ event_id=event_id
166
+ )
167
+
168
+ # Output success
169
+ reasons = decision.get("reasons", [])
170
+ agent_message = "Prompt submission approved: " + "; ".join(
171
+ reasons) if reasons else "Prompt submission approved by security policy"
172
+ output_result(logger, config.output_format, "continue", True,
173
+ "Prompt approved", agent_message)
174
+
175
+ except Exception as e:
176
+ # Decision enforcement failed - block
177
+ error_msg = str(e)
178
+ user_message = "Prompt blocked by security policy"
179
+ if "User blocked" in error_msg or "User denied" in error_msg:
180
+ user_message = "Prompt blocked by user"
181
+
182
+ output_result(logger, config.output_format, "continue", False, user_message, error_msg)
183
+
184
+ except Exception as e:
185
+ logger.error(f"Unexpected error in prompt submit handler: {e}", exc_info=True)
186
+ output_error(logger, config.output_format, "continue", f"Unexpected error: {str(e)}")
@@ -0,0 +1,170 @@
1
+ """
2
+ Shared logic for beforeReadFile/PreReadFile hook
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ from modules.logs.audit_trail import AuditTrailLogger
8
+ from modules.logs.logger import MCPLogger
9
+ from modules.redaction import redact
10
+ from modules.utils.ids import get_session_id, read_app_uid, get_project_mcpower_dir
11
+ from .output import output_result, output_error
12
+ from .types import HookConfig
13
+ from .utils import create_validator, process_attachments_for_redaction, inspect_and_enforce
14
+
15
+
16
+ async def handle_read_file(
17
+ logger: MCPLogger,
18
+ audit_logger: AuditTrailLogger,
19
+ stdin_input: str,
20
+ prompt_id: str,
21
+ event_id: str,
22
+ cwd: Optional[str],
23
+ config: HookConfig,
24
+ tool_name: str
25
+ ) -> None:
26
+ """
27
+ Shared handler for file read hooks
28
+
29
+ Args:
30
+ logger: Logger instance
31
+ audit_logger: Audit logger instance
32
+ stdin_input: Raw JSON input
33
+ prompt_id: Prompt/conversation ID
34
+ event_id: Event/generation ID
35
+ cwd: Current working directory
36
+ config: IDE-specific hook configuration
37
+ tool_name: IDE-specific tool name (e.g., "beforeReadFile", "PreToolUse")
38
+ """
39
+ session_id = get_session_id()
40
+ logger.info(f"Read file handler started (client={config.client_name}, prompt_id={prompt_id}, event_id={event_id}, cwd={cwd})")
41
+
42
+ app_uid = read_app_uid(logger, get_project_mcpower_dir(cwd))
43
+ audit_logger.set_app_uid(app_uid)
44
+
45
+ try:
46
+ # Validate input
47
+ try:
48
+ validator = create_validator(
49
+ required_fields={"file_path": str, "content": str},
50
+ optional_fields={"attachments": list}
51
+ )
52
+ input_data = validator(stdin_input)
53
+ file_path = input_data["file_path"]
54
+ provided_content = input_data["content"]
55
+ attachments = input_data.get("attachments", [])
56
+ except ValueError as e:
57
+ logger.error(f"Input validation error: {e}")
58
+ output_error(logger, config.output_format, "permission", str(e))
59
+ return
60
+
61
+ # Log audit event
62
+ audit_logger.log_event(
63
+ "agent_request",
64
+ {
65
+ "server": config.server_name,
66
+ "tool": tool_name,
67
+ "params": {"file_path": file_path, "attachments_count": len(attachments)}
68
+ },
69
+ event_id=event_id
70
+ )
71
+
72
+ # Check content length - skip API if too large
73
+ if len(provided_content) > config.max_content_length:
74
+ logger.info(f"Content length ({len(provided_content)} chars) exceeds max ({config.max_content_length}) - "
75
+ f"skipping API call")
76
+
77
+ audit_logger.log_event(
78
+ "agent_request_forwarded",
79
+ {
80
+ "server": config.server_name,
81
+ "tool": tool_name,
82
+ "params": {
83
+ "file_path": file_path,
84
+ "content_length": len(provided_content),
85
+ "content_too_large": True
86
+ }
87
+ },
88
+ event_id=event_id
89
+ )
90
+
91
+ output_result(logger, config.output_format, "permission", True)
92
+ return
93
+
94
+ # Redact the main content
95
+ redacted_content = redact(provided_content)
96
+
97
+ # Process attachments for redaction status
98
+ files_with_redactions = process_attachments_for_redaction(attachments, logger)
99
+ files_with_redactions_paths = {f["file_path"] for f in files_with_redactions}
100
+
101
+ # Build attachments info with redaction status
102
+ attachments_info = []
103
+ for attachment in attachments:
104
+ att_path = attachment.get("file_path") or attachment.get("filePath")
105
+ if att_path:
106
+ attachments_info.append({
107
+ "file_path": att_path,
108
+ "has_redactions": att_path in files_with_redactions_paths
109
+ })
110
+
111
+ logger.info(f"Processed file and {len(attachments)} attachment(s), found redactions in "
112
+ f"{len(files_with_redactions)} attachment(s)")
113
+
114
+ # Build content_data with file_path, redacted content, and attachments
115
+ content_data = {
116
+ "file_path": file_path,
117
+ "content": redacted_content,
118
+ "attachments": attachments_info
119
+ }
120
+
121
+ # Call security API and enforce decision
122
+ try:
123
+ decision = await inspect_and_enforce(
124
+ is_request=True,
125
+ session_id=session_id,
126
+ logger=logger,
127
+ audit_logger=audit_logger,
128
+ app_uid=app_uid,
129
+ event_id=event_id,
130
+ server_name=config.server_name,
131
+ tool_name=tool_name,
132
+ content_data=content_data,
133
+ prompt_id=prompt_id,
134
+ cwd=cwd,
135
+ current_files=[file_path],
136
+ client_name=config.client_name
137
+ )
138
+
139
+ # Log audit event for forwarding
140
+ audit_logger.log_event(
141
+ "agent_request_forwarded",
142
+ {
143
+ "server": config.server_name,
144
+ "tool": tool_name,
145
+ "params": {
146
+ "file_path": file_path,
147
+ "content_length": len(provided_content),
148
+ "attachments_with_redactions": len(files_with_redactions)}
149
+ },
150
+ event_id=event_id
151
+ )
152
+
153
+ # Output success
154
+ reasons = decision.get("reasons", [])
155
+ agent_message = "File read approved: " + "; ".join(reasons) if reasons else "File read approved by security policy"
156
+ output_result(logger, config.output_format, "permission", True, "File read approved", agent_message)
157
+
158
+ except Exception as e:
159
+ # Decision enforcement failed - block
160
+ error_msg = str(e)
161
+ user_message = "File read blocked by security policy"
162
+ if "User blocked" in error_msg or "User denied" in error_msg:
163
+ user_message = "File read blocked by user"
164
+
165
+ output_result(logger, config.output_format, "permission", False, user_message, error_msg)
166
+
167
+ except Exception as e:
168
+ logger.error(f"Unexpected error in read file handler: {e}", exc_info=True)
169
+ output_error(logger, config.output_format, "permission", f"Unexpected error: {str(e)}")
170
+