mcpower-proxy 0.0.58__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.
- ide_tools/__init__.py +12 -0
- ide_tools/common/__init__.py +6 -0
- ide_tools/common/hooks/__init__.py +6 -0
- ide_tools/common/hooks/init.py +125 -0
- ide_tools/common/hooks/output.py +64 -0
- ide_tools/common/hooks/prompt_submit.py +186 -0
- ide_tools/common/hooks/read_file.py +170 -0
- ide_tools/common/hooks/shell_execution.py +196 -0
- ide_tools/common/hooks/types.py +35 -0
- ide_tools/common/hooks/utils.py +276 -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 +100 -0
- ide_tools/router.py +48 -0
- main.py +11 -4
- {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/METADATA +15 -3
- mcpower_proxy-0.0.73.dist-info/RECORD +59 -0
- {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.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 +22 -17
- modules/logs/logger.py +14 -18
- 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 +55 -10
- modules/utils/json.py +3 -3
- wrapper/__version__.py +1 -1
- wrapper/middleware.py +121 -210
- wrapper/server.py +19 -11
- mcpower_proxy-0.0.58.dist-info/RECORD +0 -43
- {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/WHEEL +0 -0
- {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/entry_points.txt +0 -0
- {mcpower_proxy-0.0.58.dist-info → mcpower_proxy-0.0.73.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
cwd=cwd,
|
|
72
|
+
server_name=CURSOR_CONFIG.server_name,
|
|
73
|
+
client_name="cursor",
|
|
74
|
+
hooks=CURSOR_HOOKS
|
|
75
|
+
))
|
|
76
|
+
elif hook_event_name == "beforeShellExecution":
|
|
77
|
+
asyncio.run(
|
|
78
|
+
handle_shell_execution(logger, audit_logger, stdin_input, prompt_id, event_id, cwd, CURSOR_CONFIG,
|
|
79
|
+
hook_event_name, is_request=True))
|
|
80
|
+
elif hook_event_name == "afterShellExecution":
|
|
81
|
+
asyncio.run(
|
|
82
|
+
handle_shell_execution(logger, audit_logger, stdin_input, prompt_id, event_id, cwd, CURSOR_CONFIG,
|
|
83
|
+
hook_event_name, is_request=False))
|
|
84
|
+
elif hook_event_name == "beforeReadFile":
|
|
85
|
+
asyncio.run(handle_read_file(logger, audit_logger, stdin_input, prompt_id, event_id, cwd, CURSOR_CONFIG,
|
|
86
|
+
hook_event_name))
|
|
87
|
+
elif hook_event_name == "beforeSubmitPrompt":
|
|
88
|
+
asyncio.run(
|
|
89
|
+
handle_prompt_submit(logger, audit_logger, stdin_input, prompt_id, event_id, cwd, CURSOR_CONFIG,
|
|
90
|
+
hook_event_name))
|
|
91
|
+
else:
|
|
92
|
+
logger.error(f"Unknown hook_event_name: {hook_event_name}")
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
|
|
95
|
+
except json.JSONDecodeError as e:
|
|
96
|
+
logger.error(f"Failed to parse input JSON: {e}")
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Routing error: {e}", exc_info=True)
|
|
100
|
+
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.73
|
|
4
4
|
Summary: MCPower Security proxy
|
|
5
5
|
Author-email: MCPower Security <support@mcpower.tech>
|
|
6
6
|
License: Apache License
|
|
@@ -209,20 +209,31 @@ 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
|
|
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.3
|
|
220
220
|
Dynamic: license-file
|
|
221
221
|
|
|
222
222
|
# MCPower Proxy
|
|
223
223
|
|
|
224
224
|
Real-time semantic monitoring of AI agent<->MCP Server communication to protect from data leaks and malicious prompt injections.
|
|
225
225
|
|
|
226
|
+
## 🚀 How to use
|
|
227
|
+
|
|
228
|
+
The simplest way to use MCPower is to install the VS Code/Cursor extension:
|
|
229
|
+
|
|
230
|
+
- **VS Code Marketplace**: [Install MCPower](https://marketplace.visualstudio.com/items?itemName=mcpower.mcpower)
|
|
231
|
+
- **Open VSX (Cursor & others)**: [Install MCPower](https://open-vsx.org/extension/mcpower/mcpower)
|
|
232
|
+
|
|
233
|
+
The extension automatically installs and protects all your MCP servers - no manual configuration needed!
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
226
237
|
## Overview
|
|
227
238
|
|
|
228
239
|
MCPower is a semantic policy broker that understands *what* your AI agents are doing, not just *where* they're sending data. It acts as an intelligent security layer that intercepts every MCP tool call made by AI agents, analyzes the payload for sensitive information in real-time, and enforces security policies seamlessly.
|
|
@@ -248,3 +259,4 @@ MCPower is built as a Python-based proxy server that wraps MCP servers and provi
|
|
|
248
259
|
|
|
249
260
|
**VSC Extension**: See [targets/vsc-extension/README.md](targets/vsc-extension/README.md) for installation and user guide
|
|
250
261
|
|
|
262
|
+
<!-- mcp-name: io.github.ai-mcpower/mcpower-proxy -->
|
|
@@ -0,0 +1,59 @@
|
|
|
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=ALhDYMw8v9tlYVEVcx3kP-e6c0yEyRQAQYe7sqwHHB4,66
|
|
5
|
+
ide_tools/common/hooks/__init__.py,sha256=px5Ibzn6rlBcprp7RiZwGWnnWJmGITb0ApiXWKbeyRM,62
|
|
6
|
+
ide_tools/common/hooks/init.py,sha256=QH3qhXqOfQj1IzAXRxgDXu2zDnLHJ-_3PFZWq6Zp-TI,4044
|
|
7
|
+
ide_tools/common/hooks/output.py,sha256=xmOGmPLXy4H3A2G2A95WxqIB4qCWbVO9sq-pz2clyzU,1856
|
|
8
|
+
ide_tools/common/hooks/prompt_submit.py,sha256=-3rwH_e_uywAN9PV_IpSAOEeU21DMJ15PyMLlmwWiPA,7325
|
|
9
|
+
ide_tools/common/hooks/read_file.py,sha256=d8p_IpGwgWG9Vn0yd78KPRzoDi3RacmqxiTaFqndBa0,6481
|
|
10
|
+
ide_tools/common/hooks/shell_execution.py,sha256=uneYaiU8YQhzEjishnSW3SZGdU4Ob0hgSgo7TK5InQw,7154
|
|
11
|
+
ide_tools/common/hooks/types.py,sha256=nT_3rZ7J8b70o2FcY1lkYSFI4Z3WfD7oQgUEGSiFIbQ,1036
|
|
12
|
+
ide_tools/common/hooks/utils.py,sha256=R_jKQdG98oQhooMXV_l2xURIvYgi0Y9UwZyGbcx0LuU,9144
|
|
13
|
+
ide_tools/cursor/__init__.py,sha256=YW_V8m0A0bou0wQW_wy3nt2L_7MaNWeNKYBx-NQkilw,153
|
|
14
|
+
ide_tools/cursor/constants.py,sha256=KMRBRqxRxTdemMoq81TVG6avt3ATtsPo4aJo8XhSctk,1804
|
|
15
|
+
ide_tools/cursor/format.py,sha256=Lh-KH1IlsLL-0B98Bz-ywxpwXL8urK7yPteZGBTPOiY,972
|
|
16
|
+
ide_tools/cursor/router.py,sha256=AmRv1kfDEd3EULSlRjj0BDtedlyqtgFgSBp25iMxJBI,3856
|
|
17
|
+
mcpower_proxy-0.0.73.dist-info/licenses/LICENSE,sha256=U6WUzdnBrbmVxBmY75ikW-KtinwYnowZ7yNb5hECrvY,11337
|
|
18
|
+
modules/__init__.py,sha256=mJglXQwSRhU-bBv4LXgfu7NfGN9K4BeQWMPApen5rAA,30
|
|
19
|
+
modules/decision_handler.py,sha256=P8isKzf4GIWz9SK-VJPtO8VJEgNp7rAIcVZngnaLHmw,9574
|
|
20
|
+
modules/apis/__init__.py,sha256=Y5WZpKJzHpnRJebk0F80ZRTjR2PpA2LlYLgqI3XlmRo,15
|
|
21
|
+
modules/apis/security_policy.py,sha256=3fljaTpzfh_Nj0-jVvbnCJBL-CZxvqFNWfSlrAawsBc,15103
|
|
22
|
+
modules/logs/__init__.py,sha256=dpboUQjuO02z8K-liCbm2DYkCa-CB_ZDV9WSSjNm7Fs,15
|
|
23
|
+
modules/logs/audit_trail.py,sha256=_jE9A3WG7ooPUphDHpUFnZ6de16ewhdXYV-tV2__zxo,6183
|
|
24
|
+
modules/logs/logger.py,sha256=MJS0P8VEzUX-5udzQitznaBPCBAcZJCygUgwaDWSq94,4087
|
|
25
|
+
modules/redaction/__init__.py,sha256=e5NTmp-zonUdzzscih-w_WQ-X8Nvb8CE8b_d6SbrwWg,316
|
|
26
|
+
modules/redaction/constants.py,sha256=xbDSX8n72FuJu6JJ_sbBE0f5OcWuwEwHxBZuK9Xz-TI,1213
|
|
27
|
+
modules/redaction/gitleaks_rules.py,sha256=8dRb4g5OQaHAjx8vpMbxwu06CdDE39aqw9eqLiCDcqY,46411
|
|
28
|
+
modules/redaction/pii_rules.py,sha256=-JhjcCjH5NFeOfQGzTFNdx_-s_0i6tZ-XFxydtkByD0,10019
|
|
29
|
+
modules/redaction/redactor.py,sha256=Y3PSxXJSLJZj8gkKZ3OQ7XxoIPUilFk6gY0dbeqfjwE,23352
|
|
30
|
+
modules/ui/__init__.py,sha256=YlW4XfQEGW1ezg3UFU59nHw95LicmlpNPhim5IqSB50,34
|
|
31
|
+
modules/ui/classes.py,sha256=ZvVRdzO_hD4WnpS3_eVa0WCyaooXiYVpHLzQkzBaH6M,1777
|
|
32
|
+
modules/ui/confirmation.py,sha256=y2A8j5_z64cDdbvUEh4lI8iTgTFBcTjVbIXioMfmkpw,7740
|
|
33
|
+
modules/ui/simple_dialog.py,sha256=PZW3WSPUVtnGXx-Kkg6hTQTr5NvpTQVhgHyro1z_3aY,3900
|
|
34
|
+
modules/ui/xdialog/__init__.py,sha256=KYQKVF6pGrwc99swRBxtWVXM__j9kVX_r6KikzbCOM4,9359
|
|
35
|
+
modules/ui/xdialog/constants.py,sha256=UjtqzT_O3OHUXJOyeTGroOUnaxdVyYukf7kK6vj1rog,200
|
|
36
|
+
modules/ui/xdialog/mac_dialogs.py,sha256=6r3hkJzJJdHSt-aH1Hy4lZ1MEuZK4Kc5D_YiWglKHAA,6129
|
|
37
|
+
modules/ui/xdialog/tk_dialogs.py,sha256=isxxN_mvZUFUQu8RD1J-GC7UMH2spqR3v_domgRbczQ,2403
|
|
38
|
+
modules/ui/xdialog/windows_custom_dialog.py,sha256=tcdo35d4ZoBydAj-4yzzgW2luw97-Sdjsr3X_3-a7jM,14849
|
|
39
|
+
modules/ui/xdialog/windows_dialogs.py,sha256=ohOoK4ciyv2s4BC9r7-zvGL6mECM-RCPTVOmzDnD6VQ,7626
|
|
40
|
+
modules/ui/xdialog/windows_structs.py,sha256=xzG44OGT5hBFnimJgOLXZBhmpQ_9CFxjtz-QNjP-VCw,8698
|
|
41
|
+
modules/ui/xdialog/yad_dialogs.py,sha256=EiajZVJg-xDwYymz1fyQwLtT5DzbJR3e8plMEnOgcpo,6933
|
|
42
|
+
modules/ui/xdialog/zenity_dialogs.py,sha256=wE71I_Ovf0sjhxHVNocbrhhDd8Y8X8loLETp8TMGMPQ,4512
|
|
43
|
+
modules/utils/__init__.py,sha256=Ptwu1epT_dW6EHjGkzGHAB-MbrrmYAlcPXGGcr4PvwE,20
|
|
44
|
+
modules/utils/cli.py,sha256=5QLwsZNMKdyh3xt4NDk20vCGD-eqZ514LnVWA0J0Lbg,2414
|
|
45
|
+
modules/utils/config.py,sha256=YuGrIYfBsOYABWjFoZosObPz-R7Wdul16RnDed_glYI,6654
|
|
46
|
+
modules/utils/copy.py,sha256=9OJIqWn8PxPZXr3DTt_01jp0YgmPimckab1969WFh0c,1075
|
|
47
|
+
modules/utils/ids.py,sha256=rhhRz7RmFjuJGYLft1erHz7vJII1DRpr3iHxBhhFl1s,5743
|
|
48
|
+
modules/utils/json.py,sha256=OA-JtSBqh9qd1yfm-iyOefNBMH3ITFUdxAkj7O_JZ-Y,4024
|
|
49
|
+
modules/utils/mcp_configs.py,sha256=DZaujZnF9LlPDJHzyepH7fWSt1GTr-FEmShPCqnZ5aI,1829
|
|
50
|
+
wrapper/__init__.py,sha256=OJUsuWSoN1JqIHq4bSrzuL7ufcYJcwAmYCrJjLH44LM,22
|
|
51
|
+
wrapper/__version__.py,sha256=rCTF1Ssof33lV_ElQ2Jd40DYgONJlHNMKTaVGHBP-go,82
|
|
52
|
+
wrapper/middleware.py,sha256=77mZ30DApRlcX7__JzNFlqwKvR1mBYQg5izHbpZiNnw,29854
|
|
53
|
+
wrapper/schema.py,sha256=O-CtKI9eJ4eEnqeUXPCrK7QJAFJrdp_cFbmMyg452Aw,7952
|
|
54
|
+
wrapper/server.py,sha256=zoIW_bqXV9vKZNFtD-ij0X_LtKJEjucda6lQI5mU6qY,3440
|
|
55
|
+
mcpower_proxy-0.0.73.dist-info/METADATA,sha256=XZE3Lk26Hb6MLROM7HbBm9CbJApBPcw-N6U_-3eZyak,15669
|
|
56
|
+
mcpower_proxy-0.0.73.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
57
|
+
mcpower_proxy-0.0.73.dist-info/entry_points.txt,sha256=0smL8dxE7ERNz6XEggNaUC3QzKp8mD-v4q5nVEo0MXE,48
|
|
58
|
+
mcpower_proxy-0.0.73.dist-info/top_level.txt,sha256=OcCYMHHqbZq3mP5IZDRGdHUNOsKhT1XVnk_mDF_49Es,31
|
|
59
|
+
mcpower_proxy-0.0.73.dist-info/RECORD,,
|
modules/apis/security_policy.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Security Policy API Client"""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import time
|
|
4
5
|
import uuid
|
|
5
6
|
from typing import Dict, Any, Optional, List
|
|
6
|
-
import time
|
|
7
7
|
|
|
8
8
|
import httpx
|
|
9
9
|
|
|
@@ -13,6 +13,7 @@ from modules.logs.logger import MCPLogger
|
|
|
13
13
|
from modules.redaction import redact
|
|
14
14
|
from modules.utils.config import get_api_url, get_user_id
|
|
15
15
|
from modules.utils.json import safe_json_dumps, to_dict
|
|
16
|
+
from wrapper.__version__ import __version__
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class SecurityAPIError(Exception):
|
|
@@ -22,6 +23,7 @@ class SecurityAPIError(Exception):
|
|
|
22
23
|
|
|
23
24
|
class RateLimitExhaustedError(SecurityAPIError):
|
|
24
25
|
"""Security API rate limit exhausted (429) error"""
|
|
26
|
+
|
|
25
27
|
def __init__(self, message: str, retry_after: int = None):
|
|
26
28
|
super().__init__(message)
|
|
27
29
|
self.retry_after = retry_after
|
|
@@ -52,7 +54,7 @@ class SecurityPolicyClient:
|
|
|
52
54
|
if self.client:
|
|
53
55
|
await self.client.aclose()
|
|
54
56
|
|
|
55
|
-
async def inspect_policy_request(self, policy_request: PolicyRequest,
|
|
57
|
+
async def inspect_policy_request(self, policy_request: PolicyRequest,
|
|
56
58
|
prompt_id: str) -> InspectDecision:
|
|
57
59
|
"""Call inspect_policy_request API endpoint"""
|
|
58
60
|
if not self.client:
|
|
@@ -155,7 +157,7 @@ class SecurityPolicyClient:
|
|
|
155
157
|
audit_payload = {"payload": {"server": payload_dict["server"], "tools": payload_dict["tools"]}}
|
|
156
158
|
else:
|
|
157
159
|
audit_payload = {"payload": payload_dict}
|
|
158
|
-
|
|
160
|
+
|
|
159
161
|
self.audit_logger.log_event(
|
|
160
162
|
audit_event_type,
|
|
161
163
|
audit_payload,
|
|
@@ -166,6 +168,7 @@ class SecurityPolicyClient:
|
|
|
166
168
|
|
|
167
169
|
headers = {
|
|
168
170
|
"Content-Type": "application/json",
|
|
171
|
+
"User-Agent": f"MCPower-{__version__}",
|
|
169
172
|
"X-User-UID": self.user_id,
|
|
170
173
|
"X-App-UID": self.app_id
|
|
171
174
|
}
|
|
@@ -188,7 +191,8 @@ class SecurityPolicyClient:
|
|
|
188
191
|
raise SecurityAPIError(f"Unsupported HTTP method: {method}. Supported methods: POST, PUT")
|
|
189
192
|
|
|
190
193
|
on_make_request_duration = time.time() - on_make_request_start_time
|
|
191
|
-
self.logger.
|
|
194
|
+
self.logger.debug(
|
|
195
|
+
f"PROFILE: {method} id: {id} make_request duration: {on_make_request_duration:.2f} seconds url: {url}")
|
|
192
196
|
|
|
193
197
|
match response.status_code:
|
|
194
198
|
case 200:
|
|
@@ -215,7 +219,7 @@ class SecurityPolicyClient:
|
|
|
215
219
|
else:
|
|
216
220
|
# Other responses (e.g., /init) - log entire response
|
|
217
221
|
audit_result = {"result": data_dict}
|
|
218
|
-
|
|
222
|
+
|
|
219
223
|
self.audit_logger.log_event(
|
|
220
224
|
f"{audit_event_type}_result",
|
|
221
225
|
audit_result,
|
|
@@ -278,7 +282,8 @@ class SecurityPolicyClient:
|
|
|
278
282
|
def _handle_quota_restoration(self, endpoint: str):
|
|
279
283
|
"""Handle quota restoration (when non-429 response received)"""
|
|
280
284
|
if self.session_id in self._session_notification_times:
|
|
281
|
-
self.logger.info(
|
|
285
|
+
self.logger.info(
|
|
286
|
+
f"Quota restored - received successful response from {endpoint}. Session: {self.session_id}")
|
|
282
287
|
del self._session_notification_times[self.session_id]
|
|
283
288
|
|
|
284
289
|
def _send_throttled_quota_notification(self, retry_after: int, endpoint: str):
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decision Handler - Common decision enforcement logic
|
|
3
|
+
|
|
4
|
+
Shared module for enforcing security policy decisions across middleware and IDE tools.
|
|
5
|
+
Handles user confirmation dialogs and decision recording.
|
|
6
|
+
"""
|
|
7
|
+
from typing import Dict, Any, Optional
|
|
8
|
+
|
|
9
|
+
from mcpower_shared.mcp_types import UserConfirmation
|
|
10
|
+
from modules.apis.security_policy import SecurityPolicyClient
|
|
11
|
+
from modules.logs.audit_trail import AuditTrailLogger
|
|
12
|
+
from modules.logs.logger import MCPLogger
|
|
13
|
+
from modules.ui.classes import ConfirmationRequest, DialogOptions, UserDecision
|
|
14
|
+
from modules.ui.confirmation import UserConfirmationDialog, UserConfirmationError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DecisionEnforcementError(Exception):
|
|
18
|
+
"""Error raised when a security decision blocks an operation"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DecisionHandler:
|
|
23
|
+
"""
|
|
24
|
+
Handles security policy decision enforcement with user confirmation support.
|
|
25
|
+
|
|
26
|
+
This class provides common functionality for:
|
|
27
|
+
- Enforcing allow/block/confirm decisions
|
|
28
|
+
- Showing user confirmation dialogs
|
|
29
|
+
- Recording user decisions via API
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, logger: MCPLogger, audit_logger: AuditTrailLogger,
|
|
33
|
+
session_id: str, app_id: str):
|
|
34
|
+
self.logger = logger
|
|
35
|
+
self.audit_logger = audit_logger
|
|
36
|
+
self.session_id = session_id
|
|
37
|
+
self.app_id = app_id
|
|
38
|
+
|
|
39
|
+
async def enforce_decision(
|
|
40
|
+
self,
|
|
41
|
+
decision: Dict[str, Any],
|
|
42
|
+
is_request: bool,
|
|
43
|
+
event_id: str,
|
|
44
|
+
tool_name: str,
|
|
45
|
+
content_data: Dict[str, Any],
|
|
46
|
+
operation_type: str,
|
|
47
|
+
prompt_id: str,
|
|
48
|
+
server_name: str,
|
|
49
|
+
error_message_prefix: Optional[str] = None
|
|
50
|
+
) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Enforce security decision with user confirmation support.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
decision: Security decision dict with 'decision', 'reasons', 'severity', 'call_type'
|
|
56
|
+
is_request: True if inspecting request, False if inspecting response
|
|
57
|
+
event_id: Event ID for tracking
|
|
58
|
+
tool_name: Name of the tool/operation
|
|
59
|
+
content_data: Data to show in confirmation dialog
|
|
60
|
+
operation_type: Type of operation (e.g., 'tool', 'hook')
|
|
61
|
+
prompt_id: prompt ID for correlation
|
|
62
|
+
server_name: server name for display
|
|
63
|
+
error_message_prefix: Optional prefix for error messages
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
DecisionEnforcementError: If decision blocks the operation
|
|
67
|
+
"""
|
|
68
|
+
decision_type = decision.get("decision", "block")
|
|
69
|
+
|
|
70
|
+
if decision_type == "allow":
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
elif decision_type == "block":
|
|
74
|
+
policy_reasons = decision.get("reasons", ["Policy violation"])
|
|
75
|
+
severity = decision.get("severity", "unknown")
|
|
76
|
+
call_type = decision.get("call_type")
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
# Show a blocking dialog and wait for user decision
|
|
80
|
+
confirmation_request = ConfirmationRequest(
|
|
81
|
+
is_request=is_request,
|
|
82
|
+
tool_name=tool_name,
|
|
83
|
+
policy_reasons=policy_reasons,
|
|
84
|
+
content_data=content_data,
|
|
85
|
+
severity=severity,
|
|
86
|
+
event_id=event_id,
|
|
87
|
+
operation_type=operation_type,
|
|
88
|
+
server_name=server_name,
|
|
89
|
+
timeout_seconds=60
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
response = UserConfirmationDialog(
|
|
93
|
+
self.logger, self.audit_logger
|
|
94
|
+
).request_blocking_confirmation(confirmation_request, prompt_id, call_type)
|
|
95
|
+
|
|
96
|
+
# If we got here, user chose "Allow Anyway"
|
|
97
|
+
self.logger.info(f"User chose to 'allow anyway' a blocked {confirmation_request.operation_type} "
|
|
98
|
+
f"operation for tool '{tool_name}' (event: {event_id})")
|
|
99
|
+
|
|
100
|
+
await self._record_user_confirmation(event_id, is_request, response.user_decision, prompt_id, call_type)
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
except UserConfirmationError as e:
|
|
104
|
+
# User chose to block or dialog failed
|
|
105
|
+
await self._record_user_confirmation(event_id, is_request, UserDecision.BLOCK, prompt_id, call_type)
|
|
106
|
+
error_msg = error_message_prefix or "Security Violation"
|
|
107
|
+
raise DecisionEnforcementError(f"{error_msg}. User blocked the operation")
|
|
108
|
+
|
|
109
|
+
elif decision_type == "required_explicit_user_confirmation":
|
|
110
|
+
policy_reasons = decision.get("reasons", ["Security policy requires confirmation"])
|
|
111
|
+
severity = decision.get("severity", "unknown")
|
|
112
|
+
call_type = decision.get("call_type")
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
confirmation_request = ConfirmationRequest(
|
|
116
|
+
is_request=is_request,
|
|
117
|
+
tool_name=tool_name,
|
|
118
|
+
policy_reasons=policy_reasons,
|
|
119
|
+
content_data=content_data,
|
|
120
|
+
severity=severity,
|
|
121
|
+
event_id=event_id,
|
|
122
|
+
operation_type=operation_type,
|
|
123
|
+
server_name=server_name,
|
|
124
|
+
timeout_seconds=60
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# only show YES_ALWAYS if call_type exists
|
|
128
|
+
options = DialogOptions(
|
|
129
|
+
show_always_allow=(call_type is not None),
|
|
130
|
+
show_always_block=False
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
response = UserConfirmationDialog(
|
|
134
|
+
self.logger, self.audit_logger
|
|
135
|
+
).request_confirmation(confirmation_request, prompt_id, call_type, options)
|
|
136
|
+
|
|
137
|
+
# If we got here, user approved the operation
|
|
138
|
+
self.logger.info(f"User {response.user_decision.value} {confirmation_request.operation_type} "
|
|
139
|
+
f"operation for tool '{tool_name}' (event: {event_id})")
|
|
140
|
+
|
|
141
|
+
await self._record_user_confirmation(event_id, is_request, response.user_decision, prompt_id, call_type)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
except UserConfirmationError as e:
|
|
145
|
+
# User denied confirmation or dialog failed
|
|
146
|
+
await self._record_user_confirmation(event_id, is_request, UserDecision.BLOCK, prompt_id, call_type)
|
|
147
|
+
error_msg = error_message_prefix or "Security Violation"
|
|
148
|
+
raise DecisionEnforcementError(f"{error_msg}. User blocked the operation")
|
|
149
|
+
|
|
150
|
+
elif decision_type == "need_more_info":
|
|
151
|
+
stage_title = 'CLIENT REQUEST' if is_request else 'TOOL RESPONSE'
|
|
152
|
+
|
|
153
|
+
# Create an actionable error message for the AI agent
|
|
154
|
+
reasons = decision.get("reasons", [])
|
|
155
|
+
need_fields = decision.get("need_fields", [])
|
|
156
|
+
|
|
157
|
+
error_parts = [
|
|
158
|
+
f"SECURITY POLICY NEEDS MORE INFORMATION FOR REVIEWING {stage_title}:",
|
|
159
|
+
'\n'.join(reasons),
|
|
160
|
+
'' # newline
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
if need_fields:
|
|
164
|
+
# Convert server field names to wrapper field names for the AI agent
|
|
165
|
+
wrapper_field_mapping = {
|
|
166
|
+
"context.agent.intent": "__wrapper_modelIntent",
|
|
167
|
+
"context.agent.plan": "__wrapper_modelPlan",
|
|
168
|
+
"context.agent.expectedOutputs": "__wrapper_modelExpectedOutputs",
|
|
169
|
+
"context.agent.user_prompt": "__wrapper_userPrompt",
|
|
170
|
+
"context.agent.user_prompt_id": "__wrapper_userPromptId",
|
|
171
|
+
"context.agent.context_summary": "__wrapper_contextSummary",
|
|
172
|
+
"context.workspace.current_files": "__wrapper_currentFiles",
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
missing_wrapper_fields = []
|
|
176
|
+
for field in need_fields:
|
|
177
|
+
wrapper_field = wrapper_field_mapping.get(field, field)
|
|
178
|
+
missing_wrapper_fields.append(wrapper_field)
|
|
179
|
+
|
|
180
|
+
if missing_wrapper_fields:
|
|
181
|
+
error_parts.append("AFFECTED FIELDS:")
|
|
182
|
+
error_parts.extend(missing_wrapper_fields)
|
|
183
|
+
else:
|
|
184
|
+
error_parts.append("MISSING INFORMATION:")
|
|
185
|
+
error_parts.extend(need_fields)
|
|
186
|
+
|
|
187
|
+
error_parts.append("\nMANDATORY ACTIONS:")
|
|
188
|
+
error_parts.append("1. Add/Edit ALL affected fields according to the required information")
|
|
189
|
+
error_parts.append("2. Retry the tool call")
|
|
190
|
+
|
|
191
|
+
actionable_message = "\n".join(error_parts)
|
|
192
|
+
raise DecisionEnforcementError(actionable_message)
|
|
193
|
+
|
|
194
|
+
async def _record_user_confirmation(
|
|
195
|
+
self,
|
|
196
|
+
event_id: str,
|
|
197
|
+
is_request: bool,
|
|
198
|
+
user_decision: UserDecision,
|
|
199
|
+
prompt_id: str,
|
|
200
|
+
call_type: Optional[str] = None
|
|
201
|
+
):
|
|
202
|
+
"""Record user confirmation decision with the security API"""
|
|
203
|
+
try:
|
|
204
|
+
direction = "request" if is_request else "response"
|
|
205
|
+
|
|
206
|
+
user_confirmation = UserConfirmation(
|
|
207
|
+
event_id=event_id,
|
|
208
|
+
direction=direction,
|
|
209
|
+
user_decision=user_decision,
|
|
210
|
+
call_type=call_type
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
async with SecurityPolicyClient(session_id=self.session_id, logger=self.logger,
|
|
214
|
+
audit_logger=self.audit_logger, app_id=self.app_id) as client:
|
|
215
|
+
result = await client.record_user_confirmation(user_confirmation, prompt_id=prompt_id)
|
|
216
|
+
self.logger.debug(f"User confirmation recorded: {result}")
|
|
217
|
+
except Exception as e:
|
|
218
|
+
# Don't fail the operation if API call fails - just log the error
|
|
219
|
+
self.logger.error(f"Failed to record user confirmation: {e}")
|
modules/logs/audit_trail.py
CHANGED
|
@@ -50,14 +50,14 @@ class AuditTrailLogger:
|
|
|
50
50
|
Path(self.audit_file).parent.mkdir(parents=True, exist_ok=True)
|
|
51
51
|
|
|
52
52
|
def log_event(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
self,
|
|
54
|
+
event_type: str,
|
|
55
|
+
data: Dict[str, Any],
|
|
56
|
+
event_id: Optional[str] = None,
|
|
57
|
+
prompt_id: Optional[str] = None,
|
|
58
|
+
user_prompt: Optional[str] = None,
|
|
59
|
+
ignored_keys: Optional[List[str]] = None,
|
|
60
|
+
include_keys: Optional[List[str]] = None
|
|
61
61
|
):
|
|
62
62
|
"""
|
|
63
63
|
Log a single audit event
|
|
@@ -74,19 +74,20 @@ class AuditTrailLogger:
|
|
|
74
74
|
try:
|
|
75
75
|
# Convert data to dict structure (handles nested objects, dataclasses, Pydantic models)
|
|
76
76
|
data_dict = to_dict(data)
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
# Build event structure
|
|
79
79
|
event = {
|
|
80
80
|
"session_id": self.session_id,
|
|
81
81
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
82
82
|
"event_type": event_type,
|
|
83
|
-
"data": redact(data_dict, ignored_keys=ignored_keys, include_keys=include_keys)
|
|
83
|
+
"data": redact(data_dict, ignored_keys=ignored_keys, include_keys=include_keys)
|
|
84
|
+
# Redaction with optional key filtering
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
# Include prompt_id if provided (for grouping by user prompt)
|
|
87
88
|
if prompt_id:
|
|
88
89
|
event["prompt_id"] = prompt_id
|
|
89
|
-
|
|
90
|
+
|
|
90
91
|
# Include user_prompt text if provided (only needed once per prompt_id)
|
|
91
92
|
if user_prompt:
|
|
92
93
|
event["user_prompt"] = user_prompt
|
|
@@ -118,7 +119,7 @@ class AuditTrailLogger:
|
|
|
118
119
|
"app_uid": self.app_uid,
|
|
119
120
|
**{k: v for k, v in event.items() if k != "app_uid"}
|
|
120
121
|
}
|
|
121
|
-
|
|
122
|
+
|
|
122
123
|
# Atomic append to audit trail file
|
|
123
124
|
with open(self.audit_file, 'a', encoding='utf-8') as f:
|
|
124
125
|
f.write(safe_json_dumps(event_with_app_uid) + '\n')
|
|
@@ -130,17 +131,21 @@ class AuditTrailLogger:
|
|
|
130
131
|
|
|
131
132
|
This is called by the middleware after workspace roots are available.
|
|
132
133
|
All queued logs will be written with app_uid as the first key.
|
|
134
|
+
Supports updating app_uid when workspace context changes.
|
|
133
135
|
|
|
134
136
|
Args:
|
|
135
137
|
app_uid: The application UID from workspace root
|
|
136
138
|
"""
|
|
137
|
-
if self.app_uid
|
|
138
|
-
self.logger.warning(f"app_uid already set to {self.app_uid}, ignoring new value {app_uid}")
|
|
139
|
+
if self.app_uid == app_uid:
|
|
139
140
|
return
|
|
140
|
-
|
|
141
|
+
|
|
142
|
+
if self.app_uid is not None:
|
|
143
|
+
self.logger.info(f"app_uid changed from {self.app_uid} to {app_uid}")
|
|
144
|
+
else:
|
|
145
|
+
self.logger.debug(f"app_uid set to: {app_uid}")
|
|
146
|
+
|
|
141
147
|
self.app_uid = app_uid
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
|
|
144
149
|
# Flush all pending logs
|
|
145
150
|
if self._pending_logs:
|
|
146
151
|
self.logger.debug(f"Flushing {len(self._pending_logs)} queued audit logs")
|