mcpower-proxy 0.0.65__py3-none-any.whl → 0.0.79__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.
Potentially problematic release.
This version of mcpower-proxy might be problematic. Click here for more details.
- 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 +130 -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 +77 -0
- ide_tools/cursor/format.py +35 -0
- ide_tools/cursor/router.py +107 -0
- ide_tools/router.py +48 -0
- main.py +11 -4
- {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.dist-info}/METADATA +4 -3
- mcpower_proxy-0.0.79.dist-info/RECORD +62 -0
- {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.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 +55 -10
- 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 +144 -221
- wrapper/server.py +19 -11
- mcpower_proxy-0.0.65.dist-info/RECORD +0 -43
- {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.dist-info}/WHEEL +0 -0
- {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.dist-info}/entry_points.txt +0 -0
- {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.dist-info}/licenses/LICENSE +0 -0
wrapper/middleware.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
FastMCP middleware for security policy enforcement
|
|
3
3
|
Implements pre/post interception for all MCP operations
|
|
4
4
|
"""
|
|
5
|
+
import asyncio
|
|
6
|
+
import sys
|
|
5
7
|
import time
|
|
6
8
|
import urllib.parse
|
|
7
9
|
from datetime import datetime, timezone
|
|
@@ -11,22 +13,25 @@ from typing import Any, Dict, List, Optional
|
|
|
11
13
|
from fastmcp.exceptions import FastMCPError
|
|
12
14
|
from fastmcp.server.middleware.middleware import Middleware, MiddlewareContext, CallNext
|
|
13
15
|
from fastmcp.server.proxy import ProxyClient
|
|
16
|
+
from httpx import HTTPStatusError
|
|
17
|
+
from mcp import ErrorData
|
|
18
|
+
|
|
19
|
+
from mcpower_shared.mcp_types import (create_policy_request, create_policy_response, AgentContext, EnvironmentContext,
|
|
20
|
+
InitRequest,
|
|
21
|
+
ServerRef, ToolRef)
|
|
14
22
|
from modules.apis.security_policy import SecurityPolicyClient
|
|
23
|
+
from modules.decision_handler import DecisionHandler, DecisionEnforcementError
|
|
15
24
|
from modules.logs.audit_trail import AuditTrailLogger
|
|
16
25
|
from modules.logs.logger import MCPLogger
|
|
17
26
|
from modules.redaction import redact
|
|
18
|
-
from modules.ui.classes import ConfirmationRequest, DialogOptions, UserDecision
|
|
19
|
-
from modules.ui.confirmation import UserConfirmationDialog, UserConfirmationError
|
|
20
27
|
from modules.utils.copy import safe_copy
|
|
21
|
-
from modules.utils.ids import generate_event_id, get_session_id, read_app_uid
|
|
28
|
+
from modules.utils.ids import generate_event_id, get_session_id, read_app_uid, get_project_mcpower_dir
|
|
22
29
|
from modules.utils.json import safe_json_dumps, to_dict
|
|
23
30
|
from modules.utils.mcp_configs import extract_wrapped_server_info
|
|
31
|
+
from modules.utils.platform import get_client_os
|
|
32
|
+
from modules.utils.string import truncate_at
|
|
24
33
|
from wrapper.schema import merge_input_schema_with_existing
|
|
25
34
|
|
|
26
|
-
from mcpower_shared.mcp_types import (create_policy_request, create_policy_response, AgentContext, EnvironmentContext,
|
|
27
|
-
InitRequest,
|
|
28
|
-
ServerRef, ToolRef, UserConfirmation)
|
|
29
|
-
|
|
30
35
|
|
|
31
36
|
class MockContext:
|
|
32
37
|
"""Mock context for internal operations"""
|
|
@@ -52,10 +57,7 @@ class MockContext:
|
|
|
52
57
|
class SecurityMiddleware(Middleware):
|
|
53
58
|
"""FastMCP middleware for security policy enforcement"""
|
|
54
59
|
|
|
55
|
-
app_id: str = ""
|
|
56
60
|
_TOOLS_INIT_DEBOUNCE_SECONDS = 60
|
|
57
|
-
_last_tools_init_time: Optional[float] = None
|
|
58
|
-
_last_workspace_root: Optional[str] = None
|
|
59
61
|
|
|
60
62
|
def __init__(self,
|
|
61
63
|
wrapped_server_configs: dict,
|
|
@@ -71,6 +73,9 @@ class SecurityMiddleware(Middleware):
|
|
|
71
73
|
self.audit_logger = audit_logger
|
|
72
74
|
self.app_id = ""
|
|
73
75
|
self._last_workspace_root = None
|
|
76
|
+
self._last_tools_init_time: Optional[float] = None
|
|
77
|
+
self._tools_list_in_progress: Optional[asyncio.Task] = None
|
|
78
|
+
self._tools_list_lock = asyncio.Lock()
|
|
74
79
|
|
|
75
80
|
self.wrapped_server_name, self.wrapped_server_transport = (
|
|
76
81
|
extract_wrapped_server_info(self.wrapper_server_name, self.logger, self.wrapped_server_configs)
|
|
@@ -87,17 +92,36 @@ class SecurityMiddleware(Middleware):
|
|
|
87
92
|
async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
|
|
88
93
|
self.logger.info(f"on_message: {redact(safe_json_dumps(context))}")
|
|
89
94
|
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
self.
|
|
95
|
+
# Skip workspace check for `initialize` calls to avoid premature app_uid changes.
|
|
96
|
+
# The `initialize` request doesn't contain workspace data, so checking it would
|
|
97
|
+
# cause unnecessary audit log flushes before the actual workspace init arrives.
|
|
98
|
+
if context.method != "initialize":
|
|
99
|
+
# Check workspace roots and re-initialize app_uid if workspace changed
|
|
100
|
+
workspace_roots = await self._extract_workspace_roots(context)
|
|
101
|
+
current_workspace_root = get_project_mcpower_dir(workspace_roots[0] if workspace_roots else None)
|
|
102
|
+
if current_workspace_root != self._last_workspace_root:
|
|
103
|
+
self.logger.debug(
|
|
104
|
+
f"Workspace root changed from {self._last_workspace_root} to {current_workspace_root}")
|
|
105
|
+
self._last_workspace_root = current_workspace_root
|
|
106
|
+
self.app_id = read_app_uid(logger=self.logger, project_folder_path=current_workspace_root)
|
|
107
|
+
self.audit_logger.set_app_uid(self.app_id)
|
|
98
108
|
|
|
99
109
|
operation_type = "message"
|
|
100
|
-
|
|
110
|
+
|
|
111
|
+
async def call_next_wrapper(ctx):
|
|
112
|
+
try:
|
|
113
|
+
return await call_next(ctx)
|
|
114
|
+
except HTTPStatusError as e:
|
|
115
|
+
if e.response.status_code in (401, 403):
|
|
116
|
+
raise FastMCPError(ErrorData(
|
|
117
|
+
code=-32000,
|
|
118
|
+
message="Authentication required",
|
|
119
|
+
data={
|
|
120
|
+
"type": "unauthorized",
|
|
121
|
+
"details": "Please provide valid authentication credentials"
|
|
122
|
+
}
|
|
123
|
+
))
|
|
124
|
+
raise e
|
|
101
125
|
|
|
102
126
|
match context.type:
|
|
103
127
|
case "request":
|
|
@@ -114,13 +138,13 @@ class SecurityMiddleware(Middleware):
|
|
|
114
138
|
operation_type = "prompt"
|
|
115
139
|
case "tools/list":
|
|
116
140
|
# Special handling for tools/list - call /init instead of normal inspection
|
|
117
|
-
return await self._handle_tools_list(context,
|
|
118
|
-
case "resources/list" | "resources/templates/list" | "prompts/list":
|
|
119
|
-
return await
|
|
141
|
+
return await self._handle_tools_list(context, call_next_wrapper)
|
|
142
|
+
case "initialize" | "resources/list" | "resources/templates/list" | "prompts/list":
|
|
143
|
+
return await call_next_wrapper(context)
|
|
120
144
|
|
|
121
145
|
return await self._handle_operation(
|
|
122
146
|
context=context,
|
|
123
|
-
call_next=
|
|
147
|
+
call_next=call_next_wrapper,
|
|
124
148
|
error_class=FastMCPError,
|
|
125
149
|
operation_type=operation_type
|
|
126
150
|
)
|
|
@@ -151,7 +175,7 @@ class SecurityMiddleware(Middleware):
|
|
|
151
175
|
async def secure_elicitation_handler(self, message, response_type, params, context):
|
|
152
176
|
# FIXME: elicitation message, params, and context should be redacted before logging
|
|
153
177
|
self.logger.info(f"secure_elicitation_handler: "
|
|
154
|
-
f"message={str(message)
|
|
178
|
+
f"message={truncate_at(str(message), 100)}, response_type={response_type},"
|
|
155
179
|
f"params={params}, context={context}")
|
|
156
180
|
|
|
157
181
|
mock_context = MockContext(
|
|
@@ -180,15 +204,15 @@ class SecurityMiddleware(Middleware):
|
|
|
180
204
|
return await ProxyClient.default_progress_handler(progress, total, message)
|
|
181
205
|
|
|
182
206
|
async def secure_log_handler(self, log_message):
|
|
183
|
-
# FIXME: log_message should be redacted before logging,
|
|
184
|
-
self.logger.info(f"secure_log_handler: {str(log_message)
|
|
207
|
+
# FIXME: log_message should be redacted before logging,
|
|
208
|
+
self.logger.info(f"secure_log_handler: {truncate_at(str(log_message), 100)}")
|
|
185
209
|
# FIXME: log_message should be reviewed with policy before forwarding
|
|
186
|
-
|
|
210
|
+
|
|
187
211
|
# Handle case where log_message.data is a string instead of dict
|
|
188
212
|
# The default_log_handler expects data to be a dict with 'msg' and 'extra' keys
|
|
189
213
|
if hasattr(log_message, 'data') and isinstance(log_message.data, str):
|
|
190
214
|
log_message = safe_copy(log_message, {'data': {'msg': log_message.data, 'extra': None}})
|
|
191
|
-
|
|
215
|
+
|
|
192
216
|
return await ProxyClient.default_log_handler(log_message)
|
|
193
217
|
|
|
194
218
|
async def _handle_operation(self, context: MiddlewareContext, call_next, error_class, operation_type: str):
|
|
@@ -221,19 +245,28 @@ class SecurityMiddleware(Middleware):
|
|
|
221
245
|
prompt_id=prompt_id
|
|
222
246
|
)
|
|
223
247
|
on_inspect_request_duration = time.time() - on_inspect_request_start_time
|
|
224
|
-
self.logger.
|
|
248
|
+
self.logger.debug(
|
|
249
|
+
f"PROFILE: {operation_type} id: {event_id} inspect_request duration: {on_inspect_request_duration:.2f} seconds")
|
|
225
250
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
251
|
+
try:
|
|
252
|
+
await DecisionHandler(
|
|
253
|
+
logger=self.logger,
|
|
254
|
+
audit_logger=self.audit_logger,
|
|
255
|
+
session_id=self.session_id,
|
|
256
|
+
app_id=self.app_id
|
|
257
|
+
).enforce_decision(
|
|
258
|
+
decision=request_decision,
|
|
259
|
+
is_request=True,
|
|
260
|
+
event_id=event_id,
|
|
261
|
+
tool_name=tool_name,
|
|
262
|
+
content_data=tool_args,
|
|
263
|
+
operation_type=operation_type,
|
|
264
|
+
prompt_id=prompt_id,
|
|
265
|
+
server_name=self.wrapped_server_name,
|
|
266
|
+
error_message_prefix=f"{operation_type.title()} request blocked by security policy"
|
|
267
|
+
)
|
|
268
|
+
except DecisionEnforcementError as e:
|
|
269
|
+
raise error_class(str(e))
|
|
237
270
|
|
|
238
271
|
self.audit_logger.log_event(
|
|
239
272
|
"agent_request_forwarded",
|
|
@@ -250,7 +283,8 @@ class SecurityMiddleware(Middleware):
|
|
|
250
283
|
# Call wrapped MCP with cleaned context (e.g., no wrapper args)
|
|
251
284
|
result = await call_next(cleaned_context)
|
|
252
285
|
on_call_next_duration = time.time() - on_call_next_start_time
|
|
253
|
-
self.logger.
|
|
286
|
+
self.logger.debug(
|
|
287
|
+
f"PROFILE: {operation_type} id: {event_id} call_next duration: {on_call_next_duration:.2f} seconds")
|
|
254
288
|
|
|
255
289
|
response_content = self._extract_response_content(result)
|
|
256
290
|
|
|
@@ -275,19 +309,28 @@ class SecurityMiddleware(Middleware):
|
|
|
275
309
|
prompt_id=prompt_id
|
|
276
310
|
)
|
|
277
311
|
on_inspect_response_duration = time.time() - on_inspect_response_start_time
|
|
278
|
-
self.logger.
|
|
312
|
+
self.logger.debug(
|
|
313
|
+
f"PROFILE: {operation_type} id: {event_id} inspect_response duration: {on_inspect_response_duration:.2f} seconds")
|
|
279
314
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
315
|
+
try:
|
|
316
|
+
await DecisionHandler(
|
|
317
|
+
logger=self.logger,
|
|
318
|
+
audit_logger=self.audit_logger,
|
|
319
|
+
session_id=self.session_id,
|
|
320
|
+
app_id=self.app_id
|
|
321
|
+
).enforce_decision(
|
|
322
|
+
decision=response_decision,
|
|
323
|
+
is_request=False,
|
|
324
|
+
event_id=event_id,
|
|
325
|
+
tool_name=tool_name,
|
|
326
|
+
content_data=response_content,
|
|
327
|
+
operation_type=operation_type,
|
|
328
|
+
prompt_id=prompt_id,
|
|
329
|
+
server_name=self.wrapped_server_name,
|
|
330
|
+
error_message_prefix=f"{operation_type.title()} response blocked by security policy"
|
|
331
|
+
)
|
|
332
|
+
except DecisionEnforcementError as e:
|
|
333
|
+
raise error_class(str(e))
|
|
291
334
|
|
|
292
335
|
self.audit_logger.log_event(
|
|
293
336
|
"mcp_response_forwarded",
|
|
@@ -300,15 +343,30 @@ class SecurityMiddleware(Middleware):
|
|
|
300
343
|
prompt_id=prompt_id
|
|
301
344
|
)
|
|
302
345
|
on_handle_operation_duration = time.time() - on_handle_operation_start_time
|
|
303
|
-
self.logger.
|
|
346
|
+
self.logger.debug(
|
|
347
|
+
f"PROFILE: {operation_type} id: {event_id} duration: {on_handle_operation_duration:.2f} seconds")
|
|
304
348
|
return result
|
|
305
349
|
|
|
306
350
|
async def _handle_tools_list(self, context: MiddlewareContext, call_next: CallNext) -> Any:
|
|
307
|
-
"""Handle tools/list by calling /init API and modifying schemas"""
|
|
351
|
+
"""Handle tools/list by calling /init API and modifying schemas with deduplication"""
|
|
308
352
|
event_id = generate_event_id()
|
|
309
353
|
on_handle_tools_list_start_time = time.time()
|
|
310
|
-
|
|
311
|
-
|
|
354
|
+
|
|
355
|
+
async with self._tools_list_lock:
|
|
356
|
+
if not self._tools_list_in_progress or self._tools_list_in_progress.done():
|
|
357
|
+
self._tools_list_in_progress = asyncio.create_task(call_next(context))
|
|
358
|
+
shared_task = self._tools_list_in_progress
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
result = await shared_task
|
|
362
|
+
except Exception as e:
|
|
363
|
+
async with self._tools_list_lock:
|
|
364
|
+
if self._tools_list_in_progress is shared_task:
|
|
365
|
+
self._tools_list_in_progress = None
|
|
366
|
+
raise
|
|
367
|
+
self.logger.debug(
|
|
368
|
+
f"PROFILE: tools/list call_next duration: {time.time() - on_handle_tools_list_start_time:.2f} seconds id: {event_id}")
|
|
369
|
+
|
|
312
370
|
tools_list = None
|
|
313
371
|
if isinstance(result, list):
|
|
314
372
|
tools_list = result
|
|
@@ -338,11 +396,13 @@ class SecurityMiddleware(Middleware):
|
|
|
338
396
|
enhanced_result = result
|
|
339
397
|
|
|
340
398
|
on_handle_tools_list_duration = time.time() - on_handle_tools_list_start_time
|
|
341
|
-
self.logger.
|
|
399
|
+
self.logger.debug(
|
|
400
|
+
f"PROFILE: tools/list enhanced_result duration: {on_handle_tools_list_duration:.2f} seconds id: {event_id}")
|
|
342
401
|
return enhanced_result
|
|
343
402
|
|
|
344
403
|
on_handle_tools_list_duration = time.time() - on_handle_tools_list_start_time
|
|
345
|
-
self.logger.
|
|
404
|
+
self.logger.debug(
|
|
405
|
+
f"PROFILE: tools/list result duration: {on_handle_tools_list_duration:.2f} seconds id: {event_id}")
|
|
346
406
|
|
|
347
407
|
return result
|
|
348
408
|
|
|
@@ -373,7 +433,8 @@ class SecurityMiddleware(Middleware):
|
|
|
373
433
|
for tool in tools:
|
|
374
434
|
tool_ref = ToolRef(
|
|
375
435
|
name=getattr(tool, 'name', 'unknown'),
|
|
376
|
-
description=getattr(tool, 'description', '')
|
|
436
|
+
description=f"Description:\n{getattr(tool, 'description', '')}\n\n"
|
|
437
|
+
f"inputSchema:\n{safe_json_dumps(getattr(tool, 'parameters', {}))}",
|
|
377
438
|
version=getattr(tool, 'version', None)
|
|
378
439
|
)
|
|
379
440
|
tool_refs.append(tool_ref)
|
|
@@ -481,6 +542,12 @@ class SecurityMiddleware(Middleware):
|
|
|
481
542
|
file_path_prefix = 'file://'
|
|
482
543
|
if uri.startswith(file_path_prefix):
|
|
483
544
|
path = urllib.parse.unquote(uri[len(file_path_prefix):])
|
|
545
|
+
|
|
546
|
+
# Windows fix: remove leading slash before drive letter
|
|
547
|
+
# file:///C:/path becomes /C:/path, should be C:/path
|
|
548
|
+
if sys.platform == 'win32' and len(path) >= 3 and path[0] == '/' and path[2] == ':':
|
|
549
|
+
path = path[1:]
|
|
550
|
+
|
|
484
551
|
try:
|
|
485
552
|
resolved_path = str(Path(path).resolve())
|
|
486
553
|
workspace_roots.append(resolved_path)
|
|
@@ -504,9 +571,13 @@ class SecurityMiddleware(Middleware):
|
|
|
504
571
|
base_dict = await self._build_baseline_policy_dict(event_id, context, wrapper_args, tool_args)
|
|
505
572
|
policy_request = create_policy_request(
|
|
506
573
|
event_id=event_id,
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
574
|
+
server=ServerRef(
|
|
575
|
+
name=base_dict["server"]["name"],
|
|
576
|
+
transport=base_dict["server"]["transport"]
|
|
577
|
+
),
|
|
578
|
+
tool=ToolRef(
|
|
579
|
+
name=base_dict["tool"]["name"] or base_dict["tool"]["method"]
|
|
580
|
+
),
|
|
510
581
|
agent_context=base_dict["agent_context"],
|
|
511
582
|
env_context=base_dict["environment_context"],
|
|
512
583
|
arguments=tool_args,
|
|
@@ -532,9 +603,13 @@ class SecurityMiddleware(Middleware):
|
|
|
532
603
|
base_dict = await self._build_baseline_policy_dict(event_id, context, wrapper_args, tool_args)
|
|
533
604
|
policy_response = create_policy_response(
|
|
534
605
|
event_id=event_id,
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
606
|
+
server=ServerRef(
|
|
607
|
+
name=base_dict["server"]["name"],
|
|
608
|
+
transport=base_dict["server"]["transport"]
|
|
609
|
+
),
|
|
610
|
+
tool=ToolRef(
|
|
611
|
+
name=base_dict["tool"]["name"] or base_dict["tool"]["method"]
|
|
612
|
+
),
|
|
538
613
|
response_content=safe_json_dumps(result),
|
|
539
614
|
agent_context=base_dict["agent_context"],
|
|
540
615
|
env_context=base_dict["environment_context"],
|
|
@@ -582,32 +657,12 @@ class SecurityMiddleware(Middleware):
|
|
|
582
657
|
"current_files": wrapper_args.get('__wrapper_currentFiles')
|
|
583
658
|
},
|
|
584
659
|
client=self.wrapper_server_name,
|
|
585
|
-
client_version=self.wrapper_server_version
|
|
660
|
+
client_version=self.wrapper_server_version,
|
|
661
|
+
client_os=get_client_os(),
|
|
662
|
+
app_id=self.app_id,
|
|
586
663
|
)
|
|
587
664
|
}
|
|
588
665
|
|
|
589
|
-
async def _record_user_confirmation(self, event_id: str, is_request: bool, user_decision: UserDecision,
|
|
590
|
-
prompt_id: str, call_type: str = None):
|
|
591
|
-
"""Record user confirmation decision with the security API"""
|
|
592
|
-
try:
|
|
593
|
-
direction = "request" if is_request else "response"
|
|
594
|
-
|
|
595
|
-
user_confirmation = UserConfirmation(
|
|
596
|
-
event_id=event_id,
|
|
597
|
-
direction=direction,
|
|
598
|
-
user_decision=user_decision,
|
|
599
|
-
call_type=call_type
|
|
600
|
-
)
|
|
601
|
-
|
|
602
|
-
async with SecurityPolicyClient(session_id=self.session_id, logger=self.logger,
|
|
603
|
-
audit_logger=self.audit_logger, app_id=self.app_id) as client:
|
|
604
|
-
result = await client.record_user_confirmation(user_confirmation, prompt_id=prompt_id)
|
|
605
|
-
self.logger.debug(f"User confirmation recorded: {result}")
|
|
606
|
-
except Exception as e:
|
|
607
|
-
# Don't fail the operation if API call fails - just log the error
|
|
608
|
-
self.logger.error(f"Failed to record user confirmation: {e}")
|
|
609
|
-
|
|
610
|
-
|
|
611
666
|
@staticmethod
|
|
612
667
|
def _create_security_api_failure_decision(error: Exception) -> Dict[str, Any]:
|
|
613
668
|
"""Create a standard failure decision when security API is unavailable/failing/unreachable"""
|
|
@@ -617,135 +672,3 @@ class SecurityMiddleware(Middleware):
|
|
|
617
672
|
"reasons": [f"Security API unavailable: {error}"],
|
|
618
673
|
"matched_rules": ["security_api.error"]
|
|
619
674
|
}
|
|
620
|
-
|
|
621
|
-
async def _enforce_decision(self, decision: Dict[str, Any], error_class, base_message: str,
|
|
622
|
-
is_request: bool, event_id: str, tool_name: str, content_data: Dict[str, Any],
|
|
623
|
-
operation_type: str, prompt_id: str):
|
|
624
|
-
"""Enforce security decision with user confirmation support"""
|
|
625
|
-
decision_type = decision.get("decision", "block")
|
|
626
|
-
|
|
627
|
-
if decision_type == "allow":
|
|
628
|
-
return
|
|
629
|
-
|
|
630
|
-
elif decision_type == "block":
|
|
631
|
-
policy_reasons = decision.get("reasons", ["Policy violation"])
|
|
632
|
-
severity = decision.get("severity", "unknown")
|
|
633
|
-
call_type = decision.get("call_type")
|
|
634
|
-
|
|
635
|
-
try:
|
|
636
|
-
# Show a blocking dialog and wait for user decision
|
|
637
|
-
confirmation_request = ConfirmationRequest(
|
|
638
|
-
is_request=is_request,
|
|
639
|
-
tool_name=tool_name,
|
|
640
|
-
policy_reasons=policy_reasons,
|
|
641
|
-
content_data=content_data,
|
|
642
|
-
severity=severity,
|
|
643
|
-
event_id=event_id,
|
|
644
|
-
operation_type=operation_type,
|
|
645
|
-
server_name=self.wrapped_server_name,
|
|
646
|
-
timeout_seconds=60
|
|
647
|
-
)
|
|
648
|
-
|
|
649
|
-
response = UserConfirmationDialog(
|
|
650
|
-
self.logger, self.audit_logger
|
|
651
|
-
).request_blocking_confirmation(confirmation_request, prompt_id, call_type)
|
|
652
|
-
|
|
653
|
-
# If we got here, user chose "Allow Anyway"
|
|
654
|
-
self.logger.info(f"User chose to 'allow anyway' a blocked {confirmation_request.operation_type} "
|
|
655
|
-
f"operation for tool '{tool_name}' (event: {event_id})")
|
|
656
|
-
|
|
657
|
-
await self._record_user_confirmation(event_id, is_request, response.user_decision, prompt_id, call_type)
|
|
658
|
-
return
|
|
659
|
-
|
|
660
|
-
except UserConfirmationError as e:
|
|
661
|
-
# User chose to block or dialog failed
|
|
662
|
-
self.logger.warning(f"User blocking confirmation failed: {e}")
|
|
663
|
-
await self._record_user_confirmation(event_id, is_request, UserDecision.BLOCK, prompt_id, call_type)
|
|
664
|
-
reasons = "; ".join(policy_reasons)
|
|
665
|
-
raise error_class("Security Violation. User blocked the operation")
|
|
666
|
-
|
|
667
|
-
elif decision_type == "required_explicit_user_confirmation":
|
|
668
|
-
policy_reasons = decision.get("reasons", ["Security policy requires confirmation"])
|
|
669
|
-
severity = decision.get("severity", "unknown")
|
|
670
|
-
call_type = decision.get("call_type")
|
|
671
|
-
|
|
672
|
-
try:
|
|
673
|
-
confirmation_request = ConfirmationRequest(
|
|
674
|
-
is_request=is_request,
|
|
675
|
-
tool_name=tool_name,
|
|
676
|
-
policy_reasons=policy_reasons,
|
|
677
|
-
content_data=content_data,
|
|
678
|
-
severity=severity,
|
|
679
|
-
event_id=event_id,
|
|
680
|
-
operation_type=operation_type,
|
|
681
|
-
server_name=self.wrapped_server_name,
|
|
682
|
-
timeout_seconds=60
|
|
683
|
-
)
|
|
684
|
-
|
|
685
|
-
# only show YES_ALWAYS if call_type exists
|
|
686
|
-
options = DialogOptions(
|
|
687
|
-
show_always_allow=(call_type is not None),
|
|
688
|
-
show_always_block=False
|
|
689
|
-
)
|
|
690
|
-
|
|
691
|
-
response = UserConfirmationDialog(
|
|
692
|
-
self.logger, self.audit_logger
|
|
693
|
-
).request_confirmation(confirmation_request, prompt_id, call_type, options)
|
|
694
|
-
|
|
695
|
-
# If we got here, user approved the operation
|
|
696
|
-
self.logger.info(f"User {response.user_decision.value} {confirmation_request.operation_type} "
|
|
697
|
-
f"operation for tool '{tool_name}' (event: {event_id})")
|
|
698
|
-
|
|
699
|
-
await self._record_user_confirmation(event_id, is_request, response.user_decision, prompt_id, call_type)
|
|
700
|
-
return
|
|
701
|
-
|
|
702
|
-
except UserConfirmationError as e:
|
|
703
|
-
# User denied confirmation or dialog failed
|
|
704
|
-
self.logger.warning(f"User confirmation failed: {e}")
|
|
705
|
-
await self._record_user_confirmation(event_id, is_request, UserDecision.BLOCK, prompt_id, call_type)
|
|
706
|
-
raise error_class("Security Violation. User blocked the operation")
|
|
707
|
-
|
|
708
|
-
elif decision_type == "need_more_info":
|
|
709
|
-
stage_title = 'CLIENT REQUEST' if is_request else 'TOOL RESPONSE'
|
|
710
|
-
|
|
711
|
-
# Create an actionable error message for the AI agent
|
|
712
|
-
reasons = decision.get("reasons", [])
|
|
713
|
-
need_fields = decision.get("need_fields", [])
|
|
714
|
-
|
|
715
|
-
error_parts = [
|
|
716
|
-
f"SECURITY POLICY NEEDS MORE INFORMATION FOR REVIEWING {stage_title}:",
|
|
717
|
-
'\n'.join(reasons),
|
|
718
|
-
'' # newline
|
|
719
|
-
]
|
|
720
|
-
|
|
721
|
-
if need_fields:
|
|
722
|
-
# Convert server field names to wrapper field names for the AI agent
|
|
723
|
-
wrapper_field_mapping = {
|
|
724
|
-
"context.agent.intent": "__wrapper_modelIntent",
|
|
725
|
-
"context.agent.plan": "__wrapper_modelPlan",
|
|
726
|
-
"context.agent.expectedOutputs": "__wrapper_modelExpectedOutputs",
|
|
727
|
-
"context.agent.user_prompt": "__wrapper_userPrompt",
|
|
728
|
-
"context.agent.user_prompt_id": "__wrapper_userPromptId",
|
|
729
|
-
"context.agent.context_summary": "__wrapper_contextSummary",
|
|
730
|
-
"context.workspace.current_files": "__wrapper_currentFiles",
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
missing_wrapper_fields = []
|
|
734
|
-
for field in need_fields:
|
|
735
|
-
wrapper_field = wrapper_field_mapping.get(field, field)
|
|
736
|
-
missing_wrapper_fields.append(wrapper_field)
|
|
737
|
-
|
|
738
|
-
if missing_wrapper_fields:
|
|
739
|
-
error_parts.append("AFFECTED FIELDS:")
|
|
740
|
-
error_parts.extend(missing_wrapper_fields)
|
|
741
|
-
else:
|
|
742
|
-
error_parts.append("MISSING INFORMATION:")
|
|
743
|
-
error_parts.extend(need_fields)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
error_parts.append("\nMANDATORY ACTIONS:")
|
|
747
|
-
error_parts.append("1. Add/Edit ALL affected fields according to the required information")
|
|
748
|
-
error_parts.append("2. Retry the tool call")
|
|
749
|
-
|
|
750
|
-
actionable_message = "\n".join(error_parts)
|
|
751
|
-
raise error_class(actionable_message)
|
wrapper/server.py
CHANGED
|
@@ -6,10 +6,11 @@ Implements transparent 1:1 MCP proxying with security middleware
|
|
|
6
6
|
import logging
|
|
7
7
|
|
|
8
8
|
from fastmcp.server.middleware.logging import StructuredLoggingMiddleware
|
|
9
|
-
from fastmcp.server.proxy import ProxyClient, default_proxy_roots_handler, FastMCPProxy
|
|
9
|
+
from fastmcp.server.proxy import ProxyClient, default_proxy_roots_handler, FastMCPProxy, StatefulProxyClient
|
|
10
10
|
|
|
11
11
|
from modules.logs.audit_trail import AuditTrailLogger
|
|
12
12
|
from modules.logs.logger import MCPLogger
|
|
13
|
+
from modules.utils.json import safe_json_dumps
|
|
13
14
|
from .__version__ import __version__
|
|
14
15
|
from .middleware import SecurityMiddleware
|
|
15
16
|
|
|
@@ -42,7 +43,7 @@ def create_wrapper_server(wrapper_server_name: str,
|
|
|
42
43
|
logger=logger,
|
|
43
44
|
audit_logger=audit_logger
|
|
44
45
|
)
|
|
45
|
-
|
|
46
|
+
|
|
46
47
|
# Log MCPower startup to audit trail
|
|
47
48
|
audit_logger.log_event("mcpower_start", {
|
|
48
49
|
"wrapper_version": __version__,
|
|
@@ -51,16 +52,23 @@ def create_wrapper_server(wrapper_server_name: str,
|
|
|
51
52
|
})
|
|
52
53
|
|
|
53
54
|
# Create FastMCP server as proxy with our security-aware ProxyClient
|
|
55
|
+
# Use StatefulProxyClient for remote servers (mcp-remote or url-based transports)
|
|
56
|
+
config_str = safe_json_dumps(wrapped_server_configs)
|
|
57
|
+
is_remote = '"@mcpower/mcp-remote",' in config_str or '"url":' in config_str
|
|
58
|
+
backend_class = StatefulProxyClient if is_remote else ProxyClient
|
|
59
|
+
backend = backend_class(
|
|
60
|
+
wrapped_server_configs,
|
|
61
|
+
name=wrapper_server_name,
|
|
62
|
+
roots=default_proxy_roots_handler, # Use default for filesystem roots
|
|
63
|
+
sampling_handler=security_middleware.secure_sampling_handler,
|
|
64
|
+
elicitation_handler=security_middleware.secure_elicitation_handler,
|
|
65
|
+
log_handler=security_middleware.secure_log_handler,
|
|
66
|
+
progress_handler=security_middleware.secure_progress_handler,
|
|
67
|
+
)
|
|
68
|
+
|
|
54
69
|
def client_factory():
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
name=wrapper_server_name,
|
|
58
|
-
roots=default_proxy_roots_handler, # Use default for filesystem roots
|
|
59
|
-
sampling_handler=security_middleware.secure_sampling_handler,
|
|
60
|
-
elicitation_handler=security_middleware.secure_elicitation_handler,
|
|
61
|
-
log_handler=security_middleware.secure_log_handler,
|
|
62
|
-
progress_handler=security_middleware.secure_progress_handler,
|
|
63
|
-
)
|
|
70
|
+
# we must return the same instance, otherwise StatefulProxyClient doesn't play nice with mcp-remote
|
|
71
|
+
return backend
|
|
64
72
|
|
|
65
73
|
server = FastMCPProxy(client_factory=client_factory, name=wrapper_server_name, version=__version__)
|
|
66
74
|
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
main.py,sha256=4BnzO7q9Atpzgr-_NTc1loRnrRY0m5OxeG9biI-C0es,3707
|
|
2
|
-
mcpower_proxy-0.0.65.dist-info/licenses/LICENSE,sha256=U6WUzdnBrbmVxBmY75ikW-KtinwYnowZ7yNb5hECrvY,11337
|
|
3
|
-
modules/__init__.py,sha256=mJglXQwSRhU-bBv4LXgfu7NfGN9K4BeQWMPApen5rAA,30
|
|
4
|
-
modules/apis/__init__.py,sha256=Y5WZpKJzHpnRJebk0F80ZRTjR2PpA2LlYLgqI3XlmRo,15
|
|
5
|
-
modules/apis/security_policy.py,sha256=AZDHTuOf99WhhzNw9AwC-0KACx1-ZjtQx-Ve3gAKYPM,15000
|
|
6
|
-
modules/logs/__init__.py,sha256=dpboUQjuO02z8K-liCbm2DYkCa-CB_ZDV9WSSjNm7Fs,15
|
|
7
|
-
modules/logs/audit_trail.py,sha256=r8aIjaW-jBXXhSwdafzgOn0AvvdTZG8UPnkI0GmbJnA,6199
|
|
8
|
-
modules/logs/logger.py,sha256=dfYRLnABZB07SBfoYV4DsD8-ZCpzEeoewFCBGHyqo9k,4171
|
|
9
|
-
modules/redaction/__init__.py,sha256=e5NTmp-zonUdzzscih-w_WQ-X8Nvb8CE8b_d6SbrwWg,316
|
|
10
|
-
modules/redaction/constants.py,sha256=xbDSX8n72FuJu6JJ_sbBE0f5OcWuwEwHxBZuK9Xz-TI,1213
|
|
11
|
-
modules/redaction/gitleaks_rules.py,sha256=8dRb4g5OQaHAjx8vpMbxwu06CdDE39aqw9eqLiCDcqY,46411
|
|
12
|
-
modules/redaction/pii_rules.py,sha256=-JhjcCjH5NFeOfQGzTFNdx_-s_0i6tZ-XFxydtkByD0,10019
|
|
13
|
-
modules/redaction/redactor.py,sha256=jxb5itJ_xDo43XG28tXbAyMqhTmJXzAhStzAhlWvrOI,23905
|
|
14
|
-
modules/ui/__init__.py,sha256=-fZ_Bna6XnXeC7xB9loQ-7Qv2uK0NhSr-qoyRx2f8ZU,33
|
|
15
|
-
modules/ui/classes.py,sha256=ZvVRdzO_hD4WnpS3_eVa0WCyaooXiYVpHLzQkzBaH6M,1777
|
|
16
|
-
modules/ui/confirmation.py,sha256=VfVPFkttO4Mstja6dA85tqulyvdXyo8DyHzl0uiPWKU,7741
|
|
17
|
-
modules/ui/simple_dialog.py,sha256=PZW3WSPUVtnGXx-Kkg6hTQTr5NvpTQVhgHyro1z_3aY,3900
|
|
18
|
-
modules/ui/xdialog/__init__.py,sha256=KYQKVF6pGrwc99swRBxtWVXM__j9kVX_r6KikzbCOM4,9359
|
|
19
|
-
modules/ui/xdialog/constants.py,sha256=UjtqzT_O3OHUXJOyeTGroOUnaxdVyYukf7kK6vj1rog,200
|
|
20
|
-
modules/ui/xdialog/mac_dialogs.py,sha256=6r3hkJzJJdHSt-aH1Hy4lZ1MEuZK4Kc5D_YiWglKHAA,6129
|
|
21
|
-
modules/ui/xdialog/tk_dialogs.py,sha256=isxxN_mvZUFUQu8RD1J-GC7UMH2spqR3v_domgRbczQ,2403
|
|
22
|
-
modules/ui/xdialog/windows_custom_dialog.py,sha256=tcdo35d4ZoBydAj-4yzzgW2luw97-Sdjsr3X_3-a7jM,14849
|
|
23
|
-
modules/ui/xdialog/windows_dialogs.py,sha256=ohOoK4ciyv2s4BC9r7-zvGL6mECM-RCPTVOmzDnD6VQ,7626
|
|
24
|
-
modules/ui/xdialog/windows_structs.py,sha256=xzG44OGT5hBFnimJgOLXZBhmpQ_9CFxjtz-QNjP-VCw,8698
|
|
25
|
-
modules/ui/xdialog/yad_dialogs.py,sha256=EiajZVJg-xDwYymz1fyQwLtT5DzbJR3e8plMEnOgcpo,6933
|
|
26
|
-
modules/ui/xdialog/zenity_dialogs.py,sha256=wE71I_Ovf0sjhxHVNocbrhhDd8Y8X8loLETp8TMGMPQ,4512
|
|
27
|
-
modules/utils/__init__.py,sha256=Ptwu1epT_dW6EHjGkzGHAB-MbrrmYAlcPXGGcr4PvwE,20
|
|
28
|
-
modules/utils/cli.py,sha256=qYgf7TsWKjwPsCItbDYzNZCih2vfGAbAl2MIem320_Y,1517
|
|
29
|
-
modules/utils/config.py,sha256=YuGrIYfBsOYABWjFoZosObPz-R7Wdul16RnDed_glYI,6654
|
|
30
|
-
modules/utils/copy.py,sha256=9OJIqWn8PxPZXr3DTt_01jp0YgmPimckab1969WFh0c,1075
|
|
31
|
-
modules/utils/ids.py,sha256=i2MjU_sLFpowYb_5pYQKsbz_wJfjpDXkA9Svqy_wiJQ,4622
|
|
32
|
-
modules/utils/json.py,sha256=8GA2akQsufXIn9HIP4SkFGFShzngexEBzejXi4B-Mfg,4031
|
|
33
|
-
modules/utils/mcp_configs.py,sha256=DZaujZnF9LlPDJHzyepH7fWSt1GTr-FEmShPCqnZ5aI,1829
|
|
34
|
-
wrapper/__init__.py,sha256=OJUsuWSoN1JqIHq4bSrzuL7ufcYJcwAmYCrJjLH44LM,22
|
|
35
|
-
wrapper/__version__.py,sha256=NE6I0jrPNGiK6wuSA5kZ5ngl6YqctKwp8ps1nNeqtfo,82
|
|
36
|
-
wrapper/middleware.py,sha256=yyeYeZLnla3mVvBWUbfaIXIPkZ2BGpF_h5E_jbt-oLA,34684
|
|
37
|
-
wrapper/schema.py,sha256=O-CtKI9eJ4eEnqeUXPCrK7QJAFJrdp_cFbmMyg452Aw,7952
|
|
38
|
-
wrapper/server.py,sha256=uVtxELALRrQNd-VrPWyLQPiEzxOpG-oCU7bItAeSjYU,2981
|
|
39
|
-
mcpower_proxy-0.0.65.dist-info/METADATA,sha256=RprzfVhz0JCbj_vpP60cWXvlkTQyHlf29DKwsLHS930,15667
|
|
40
|
-
mcpower_proxy-0.0.65.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
41
|
-
mcpower_proxy-0.0.65.dist-info/entry_points.txt,sha256=0smL8dxE7ERNz6XEggNaUC3QzKp8mD-v4q5nVEo0MXE,48
|
|
42
|
-
mcpower_proxy-0.0.65.dist-info/top_level.txt,sha256=FLbRkTTggoMB-kq14IH4ZUbNGMGtbxtmiWw0QykRlkU,21
|
|
43
|
-
mcpower_proxy-0.0.65.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|