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.

Files changed (41) hide show
  1. ide_tools/__init__.py +12 -0
  2. ide_tools/common/__init__.py +5 -0
  3. ide_tools/common/hooks/__init__.py +5 -0
  4. ide_tools/common/hooks/init.py +130 -0
  5. ide_tools/common/hooks/output.py +63 -0
  6. ide_tools/common/hooks/prompt_submit.py +136 -0
  7. ide_tools/common/hooks/read_file.py +170 -0
  8. ide_tools/common/hooks/shell_execution.py +257 -0
  9. ide_tools/common/hooks/shell_parser_bashlex.py +394 -0
  10. ide_tools/common/hooks/types.py +34 -0
  11. ide_tools/common/hooks/utils.py +286 -0
  12. ide_tools/cursor/__init__.py +11 -0
  13. ide_tools/cursor/constants.py +77 -0
  14. ide_tools/cursor/format.py +35 -0
  15. ide_tools/cursor/router.py +107 -0
  16. ide_tools/router.py +48 -0
  17. main.py +11 -4
  18. {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.dist-info}/METADATA +4 -3
  19. mcpower_proxy-0.0.79.dist-info/RECORD +62 -0
  20. {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.dist-info}/top_level.txt +1 -0
  21. modules/apis/security_policy.py +11 -6
  22. modules/decision_handler.py +219 -0
  23. modules/logs/audit_trail.py +20 -18
  24. modules/logs/logger.py +14 -18
  25. modules/redaction/gitleaks_rules.py +1 -1
  26. modules/redaction/pii_rules.py +0 -48
  27. modules/redaction/redactor.py +112 -107
  28. modules/ui/__init__.py +1 -1
  29. modules/ui/confirmation.py +0 -1
  30. modules/utils/cli.py +36 -6
  31. modules/utils/ids.py +55 -10
  32. modules/utils/json.py +3 -3
  33. modules/utils/platform.py +23 -0
  34. modules/utils/string.py +17 -0
  35. wrapper/__version__.py +1 -1
  36. wrapper/middleware.py +144 -221
  37. wrapper/server.py +19 -11
  38. mcpower_proxy-0.0.65.dist-info/RECORD +0 -43
  39. {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.dist-info}/WHEEL +0 -0
  40. {mcpower_proxy-0.0.65.dist-info → mcpower_proxy-0.0.79.dist-info}/entry_points.txt +0 -0
  41. {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
- # Check workspace roots and re-initialize app_uid if workspace changed
91
- workspace_roots = await self._extract_workspace_roots(context)
92
- current_workspace_root = workspace_roots[0] if workspace_roots else str(Path.home() / ".mcpower")
93
- if current_workspace_root != self._last_workspace_root:
94
- self.logger.debug(f"Workspace root changed from {self._last_workspace_root} to {current_workspace_root}")
95
- self._last_workspace_root = current_workspace_root
96
- self.app_id = read_app_uid(logger=self.logger, project_folder_path=current_workspace_root)
97
- self.audit_logger.set_app_uid(self.app_id)
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
- call_next_callback = call_next
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, call_next)
118
- case "resources/list" | "resources/templates/list" | "prompts/list":
119
- return await call_next_callback(context)
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=call_next_callback,
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)[:100]}..., response_type={response_type},"
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)[:100]}...")
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.info(f"PROFILE: {operation_type} id: {event_id} inspect_request duration: {on_inspect_request_duration:.2f} seconds")
248
+ self.logger.debug(
249
+ f"PROFILE: {operation_type} id: {event_id} inspect_request duration: {on_inspect_request_duration:.2f} seconds")
225
250
 
226
- await self._enforce_decision(
227
- decision=request_decision,
228
- error_class=error_class,
229
- base_message=f"{operation_type.title()} request blocked by security policy",
230
- is_request=True,
231
- event_id=event_id,
232
- tool_name=tool_name,
233
- content_data=tool_args,
234
- operation_type=operation_type,
235
- prompt_id=prompt_id
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.info(f"PROFILE: {operation_type} id: {event_id} call_next duration: {on_call_next_duration:.2f} seconds")
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.info(f"PROFILE: {operation_type} id: {event_id} inspect_response duration: {on_inspect_response_duration:.2f} seconds")
312
+ self.logger.debug(
313
+ f"PROFILE: {operation_type} id: {event_id} inspect_response duration: {on_inspect_response_duration:.2f} seconds")
279
314
 
280
- await self._enforce_decision(
281
- decision=response_decision,
282
- error_class=error_class,
283
- base_message=f"{operation_type.title()} response blocked by security policy",
284
- is_request=False,
285
- event_id=event_id,
286
- tool_name=tool_name,
287
- content_data=response_content,
288
- operation_type=operation_type,
289
- prompt_id=prompt_id
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.info(f"PROFILE: {operation_type} id: {event_id} duration: {on_handle_operation_duration:.2f} seconds")
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
- result = await call_next(context)
311
- self.logger.info(f"PROFILE: tools/list call_next duration: {time.time() - on_handle_tools_list_start_time:.2f} seconds id: {event_id}")
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.info(f"PROFILE: tools/list enhanced_result duration: {on_handle_tools_list_duration:.2f} seconds id: {event_id}")
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.info(f"PROFILE: tools/list result duration: {on_handle_tools_list_duration:.2f} seconds id: {event_id}")
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
- server_name=base_dict["server"]["name"],
508
- server_transport=base_dict["server"]["transport"],
509
- tool_name=base_dict["tool"]["name"] or base_dict["tool"]["method"],
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
- server_name=base_dict["server"]["name"],
536
- server_transport=base_dict["server"]["transport"],
537
- tool_name=base_dict["tool"]["name"] or base_dict["tool"]["method"],
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 ProxyClient(
56
- wrapped_server_configs,
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,,