mcpower-proxy 0.0.67__tar.gz → 0.0.69__tar.gz

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 (49) hide show
  1. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/PKG-INFO +2 -2
  2. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/pyproject.toml +2 -2
  3. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/mcpower_proxy.egg-info/PKG-INFO +2 -2
  4. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/mcpower_proxy.egg-info/requires.txt +1 -1
  5. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/apis/security_policy.py +4 -2
  6. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/utils/cli.py +2 -4
  7. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/wrapper/__version__.py +1 -1
  8. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/wrapper/middleware.py +76 -36
  9. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/wrapper/server.py +19 -11
  10. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/LICENSE +0 -0
  11. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/README.md +0 -0
  12. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/setup.cfg +0 -0
  13. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/LICENSE +0 -0
  14. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/main.py +0 -0
  15. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/mcpower_proxy.egg-info/SOURCES.txt +0 -0
  16. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/mcpower_proxy.egg-info/dependency_links.txt +0 -0
  17. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/mcpower_proxy.egg-info/entry_points.txt +0 -0
  18. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/mcpower_proxy.egg-info/top_level.txt +0 -0
  19. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/__init__.py +0 -0
  20. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/apis/__init__.py +0 -0
  21. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/logs/__init__.py +0 -0
  22. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/logs/audit_trail.py +0 -0
  23. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/logs/logger.py +0 -0
  24. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/redaction/__init__.py +0 -0
  25. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/redaction/constants.py +0 -0
  26. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/redaction/gitleaks_rules.py +0 -0
  27. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/redaction/pii_rules.py +0 -0
  28. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/redaction/redactor.py +0 -0
  29. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/ui/__init__.py +0 -0
  30. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/ui/classes.py +0 -0
  31. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/ui/confirmation.py +0 -0
  32. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/ui/simple_dialog.py +0 -0
  33. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/ui/xdialog/__init__.py +0 -0
  34. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/ui/xdialog/constants.py +0 -0
  35. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/ui/xdialog/mac_dialogs.py +0 -0
  36. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/ui/xdialog/tk_dialogs.py +0 -0
  37. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/ui/xdialog/windows_custom_dialog.py +0 -0
  38. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/ui/xdialog/windows_dialogs.py +0 -0
  39. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/ui/xdialog/windows_structs.py +0 -0
  40. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/ui/xdialog/yad_dialogs.py +0 -0
  41. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/ui/xdialog/zenity_dialogs.py +0 -0
  42. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/utils/__init__.py +0 -0
  43. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/utils/config.py +0 -0
  44. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/utils/copy.py +0 -0
  45. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/utils/ids.py +0 -0
  46. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/utils/json.py +0 -0
  47. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/modules/utils/mcp_configs.py +0 -0
  48. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/wrapper/__init__.py +0 -0
  49. {mcpower_proxy-0.0.67 → mcpower_proxy-0.0.69}/src/wrapper/schema.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcpower-proxy
3
- Version: 0.0.67
3
+ Version: 0.0.69
4
4
  Summary: MCPower Security proxy
5
5
  Author-email: MCPower Security <support@mcpower.tech>
6
6
  License: Apache License
@@ -209,7 +209,7 @@ Keywords: mcp,security,proxy,monitoring,audit,redaction,policy-enforcement
209
209
  Requires-Python: ~=3.11.0
210
210
  Description-Content-Type: text/markdown
211
211
  License-File: LICENSE
212
- Requires-Dist: fastmcp==2.13.0.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcpower-proxy"
3
- version = "0.0.67"
3
+ version = "0.0.69"
4
4
  description = "MCPower Security proxy"
5
5
  readme = "README.md"
6
6
  requires-python = "~=3.11.0"
@@ -9,7 +9,7 @@ authors = [
9
9
  { name = "MCPower Security", email = "support@mcpower.tech" }
10
10
  ]
11
11
  dependencies = [
12
- "fastmcp==2.13.0.1",
12
+ "fastmcp==2.13.0.2",
13
13
  "httpx>=0.25.0",
14
14
  "mcp>=1.0.0",
15
15
  "watchdog>=3.0.0",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcpower-proxy
3
- Version: 0.0.67
3
+ Version: 0.0.69
4
4
  Summary: MCPower Security proxy
5
5
  Author-email: MCPower Security <support@mcpower.tech>
6
6
  License: Apache License
@@ -209,7 +209,7 @@ Keywords: mcp,security,proxy,monitoring,audit,redaction,policy-enforcement
209
209
  Requires-Python: ~=3.11.0
210
210
  Description-Content-Type: text/markdown
211
211
  License-File: LICENSE
212
- Requires-Dist: fastmcp==2.13.0.1
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
@@ -1,4 +1,4 @@
1
- fastmcp==2.13.0.1
1
+ fastmcp==2.13.0.2
2
2
  httpx>=0.25.0
3
3
  mcp>=1.0.0
4
4
  watchdog>=3.0.0
@@ -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):
@@ -166,6 +167,7 @@ class SecurityPolicyClient:
166
167
 
167
168
  headers = {
168
169
  "Content-Type": "application/json",
170
+ "User-Agent": f"MCPower-{__version__}",
169
171
  "X-User-UID": self.user_id,
170
172
  "X-App-UID": self.app_id
171
173
  }
@@ -188,7 +190,7 @@ class SecurityPolicyClient:
188
190
  raise SecurityAPIError(f"Unsupported HTTP method: {method}. Supported methods: POST, PUT")
189
191
 
190
192
  on_make_request_duration = time.time() - on_make_request_start_time
191
- self.logger.info(f"PROFILE: {method} id: {id} make_request duration: {on_make_request_duration:.2f} seconds url: {url}")
193
+ self.logger.debug(f"PROFILE: {method} id: {id} make_request duration: {on_make_request_duration:.2f} seconds url: {url}")
192
194
 
193
195
  match response.status_code:
194
196
  case 200:
@@ -7,7 +7,7 @@ import argparse
7
7
  def parse_args():
8
8
  """Parse command line arguments"""
9
9
  parser = argparse.ArgumentParser(
10
- description="MCPower - Transparent 1:1 MCP Wrapper with security enforcement",
10
+ description="Transparent MCP wrapper with security middleware for real-time policy enforcement and monitoring.",
11
11
  formatter_class=argparse.RawDescriptionHelpFormatter,
12
12
  epilog="""
13
13
  Examples:
@@ -24,10 +24,8 @@ Examples:
24
24
  %(prog)s --wrapped-config '{"command": "node", "args": ["server.js"]}' --name MyWrapper
25
25
 
26
26
  Reference Links:
27
- FastMCP Proxy: https://gofastmcp.com/servers/proxy
28
- • FastMCP Middleware: https://gofastmcp.com/servers/middleware
27
+ MCPower Proxy: https://github.com/ai-mcpower/mcpower-proxy
29
28
  • MCP Official: https://modelcontextprotocol.io
30
- • Claude MCP Config: https://docs.anthropic.com/en/docs/claude-code/mcp
31
29
  """
32
30
  )
33
31
 
@@ -3,4 +3,4 @@
3
3
  Wrapper MCP Server Version
4
4
  """
5
5
 
6
- __version__ = "0.0.67"
6
+ __version__ = "0.0.69"
@@ -2,6 +2,7 @@
2
2
  FastMCP middleware for security policy enforcement
3
3
  Implements pre/post interception for all MCP operations
4
4
  """
5
+ import asyncio
5
6
  import sys
6
7
  import time
7
8
  import urllib.parse
@@ -12,6 +13,12 @@ from typing import Any, Dict, List, Optional
12
13
  from fastmcp.exceptions import FastMCPError
13
14
  from fastmcp.server.middleware.middleware import Middleware, MiddlewareContext, CallNext
14
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, UserConfirmation)
15
22
  from modules.apis.security_policy import SecurityPolicyClient
16
23
  from modules.logs.audit_trail import AuditTrailLogger
17
24
  from modules.logs.logger import MCPLogger
@@ -24,10 +31,6 @@ from modules.utils.json import safe_json_dumps, to_dict
24
31
  from modules.utils.mcp_configs import extract_wrapped_server_info
25
32
  from wrapper.schema import merge_input_schema_with_existing
26
33
 
27
- from mcpower_shared.mcp_types import (create_policy_request, create_policy_response, AgentContext, EnvironmentContext,
28
- InitRequest,
29
- ServerRef, ToolRef, UserConfirmation)
30
-
31
34
 
32
35
  class MockContext:
33
36
  """Mock context for internal operations"""
@@ -53,10 +56,7 @@ class MockContext:
53
56
  class SecurityMiddleware(Middleware):
54
57
  """FastMCP middleware for security policy enforcement"""
55
58
 
56
- app_id: str = ""
57
59
  _TOOLS_INIT_DEBOUNCE_SECONDS = 60
58
- _last_tools_init_time: Optional[float] = None
59
- _last_workspace_root: Optional[str] = None
60
60
 
61
61
  def __init__(self,
62
62
  wrapped_server_configs: dict,
@@ -72,6 +72,9 @@ class SecurityMiddleware(Middleware):
72
72
  self.audit_logger = audit_logger
73
73
  self.app_id = ""
74
74
  self._last_workspace_root = None
75
+ self._last_tools_init_time: Optional[float] = None
76
+ self._tools_list_in_progress: Optional[asyncio.Task] = None
77
+ self._tools_list_lock = asyncio.Lock()
75
78
 
76
79
  self.wrapped_server_name, self.wrapped_server_transport = (
77
80
  extract_wrapped_server_info(self.wrapper_server_name, self.logger, self.wrapped_server_configs)
@@ -88,17 +91,36 @@ class SecurityMiddleware(Middleware):
88
91
  async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
89
92
  self.logger.info(f"on_message: {redact(safe_json_dumps(context))}")
90
93
 
91
- # Check workspace roots and re-initialize app_uid if workspace changed
92
- workspace_roots = await self._extract_workspace_roots(context)
93
- current_workspace_root = workspace_roots[0] if workspace_roots else str(Path.home() / ".mcpower")
94
- if current_workspace_root != self._last_workspace_root:
95
- self.logger.debug(f"Workspace root changed from {self._last_workspace_root} to {current_workspace_root}")
96
- self._last_workspace_root = current_workspace_root
97
- self.app_id = read_app_uid(logger=self.logger, project_folder_path=current_workspace_root)
98
- self.audit_logger.set_app_uid(self.app_id)
94
+ # Skip workspace check for `initialize` calls to avoid premature app_uid changes.
95
+ # The `initialize` request doesn't contain workspace data, so checking it would
96
+ # cause unnecessary audit log flushes before the actual workspace init arrives.
97
+ if context.method != "initialize":
98
+ # Check workspace roots and re-initialize app_uid if workspace changed
99
+ workspace_roots = await self._extract_workspace_roots(context)
100
+ current_workspace_root = workspace_roots[0] if workspace_roots else str(Path.home() / ".mcpower")
101
+ if current_workspace_root != self._last_workspace_root:
102
+ self.logger.debug(
103
+ f"Workspace root changed from {self._last_workspace_root} to {current_workspace_root}")
104
+ self._last_workspace_root = current_workspace_root
105
+ self.app_id = read_app_uid(logger=self.logger, project_folder_path=current_workspace_root)
106
+ self.audit_logger.set_app_uid(self.app_id)
99
107
 
100
108
  operation_type = "message"
101
- call_next_callback = call_next
109
+
110
+ async def call_next_wrapper(ctx):
111
+ try:
112
+ return await call_next(ctx)
113
+ except HTTPStatusError as e:
114
+ if e.response.status_code in (401, 403):
115
+ raise FastMCPError(ErrorData(
116
+ code=-32000,
117
+ message="Authentication required",
118
+ data={
119
+ "type": "unauthorized",
120
+ "details": "Please provide valid authentication credentials"
121
+ }
122
+ ))
123
+ raise e
102
124
 
103
125
  match context.type:
104
126
  case "request":
@@ -115,13 +137,13 @@ class SecurityMiddleware(Middleware):
115
137
  operation_type = "prompt"
116
138
  case "tools/list":
117
139
  # Special handling for tools/list - call /init instead of normal inspection
118
- return await self._handle_tools_list(context, call_next)
140
+ return await self._handle_tools_list(context, call_next_wrapper)
119
141
  case "initialize" | "resources/list" | "resources/templates/list" | "prompts/list":
120
- return await call_next_callback(context)
142
+ return await call_next_wrapper(context)
121
143
 
122
144
  return await self._handle_operation(
123
145
  context=context,
124
- call_next=call_next_callback,
146
+ call_next=call_next_wrapper,
125
147
  error_class=FastMCPError,
126
148
  operation_type=operation_type
127
149
  )
@@ -181,15 +203,15 @@ class SecurityMiddleware(Middleware):
181
203
  return await ProxyClient.default_progress_handler(progress, total, message)
182
204
 
183
205
  async def secure_log_handler(self, log_message):
184
- # FIXME: log_message should be redacted before logging,
206
+ # FIXME: log_message should be redacted before logging,
185
207
  self.logger.info(f"secure_log_handler: {str(log_message)[:100]}...")
186
208
  # FIXME: log_message should be reviewed with policy before forwarding
187
-
209
+
188
210
  # Handle case where log_message.data is a string instead of dict
189
211
  # The default_log_handler expects data to be a dict with 'msg' and 'extra' keys
190
212
  if hasattr(log_message, 'data') and isinstance(log_message.data, str):
191
213
  log_message = safe_copy(log_message, {'data': {'msg': log_message.data, 'extra': None}})
192
-
214
+
193
215
  return await ProxyClient.default_log_handler(log_message)
194
216
 
195
217
  async def _handle_operation(self, context: MiddlewareContext, call_next, error_class, operation_type: str):
@@ -222,7 +244,8 @@ class SecurityMiddleware(Middleware):
222
244
  prompt_id=prompt_id
223
245
  )
224
246
  on_inspect_request_duration = time.time() - on_inspect_request_start_time
225
- self.logger.info(f"PROFILE: {operation_type} id: {event_id} inspect_request duration: {on_inspect_request_duration:.2f} seconds")
247
+ self.logger.debug(
248
+ f"PROFILE: {operation_type} id: {event_id} inspect_request duration: {on_inspect_request_duration:.2f} seconds")
226
249
 
227
250
  await self._enforce_decision(
228
251
  decision=request_decision,
@@ -251,7 +274,8 @@ class SecurityMiddleware(Middleware):
251
274
  # Call wrapped MCP with cleaned context (e.g., no wrapper args)
252
275
  result = await call_next(cleaned_context)
253
276
  on_call_next_duration = time.time() - on_call_next_start_time
254
- self.logger.info(f"PROFILE: {operation_type} id: {event_id} call_next duration: {on_call_next_duration:.2f} seconds")
277
+ self.logger.debug(
278
+ f"PROFILE: {operation_type} id: {event_id} call_next duration: {on_call_next_duration:.2f} seconds")
255
279
 
256
280
  response_content = self._extract_response_content(result)
257
281
 
@@ -276,7 +300,8 @@ class SecurityMiddleware(Middleware):
276
300
  prompt_id=prompt_id
277
301
  )
278
302
  on_inspect_response_duration = time.time() - on_inspect_response_start_time
279
- self.logger.info(f"PROFILE: {operation_type} id: {event_id} inspect_response duration: {on_inspect_response_duration:.2f} seconds")
303
+ self.logger.debug(
304
+ f"PROFILE: {operation_type} id: {event_id} inspect_response duration: {on_inspect_response_duration:.2f} seconds")
280
305
 
281
306
  await self._enforce_decision(
282
307
  decision=response_decision,
@@ -301,15 +326,30 @@ class SecurityMiddleware(Middleware):
301
326
  prompt_id=prompt_id
302
327
  )
303
328
  on_handle_operation_duration = time.time() - on_handle_operation_start_time
304
- self.logger.info(f"PROFILE: {operation_type} id: {event_id} duration: {on_handle_operation_duration:.2f} seconds")
329
+ self.logger.debug(
330
+ f"PROFILE: {operation_type} id: {event_id} duration: {on_handle_operation_duration:.2f} seconds")
305
331
  return result
306
332
 
307
333
  async def _handle_tools_list(self, context: MiddlewareContext, call_next: CallNext) -> Any:
308
- """Handle tools/list by calling /init API and modifying schemas"""
334
+ """Handle tools/list by calling /init API and modifying schemas with deduplication"""
309
335
  event_id = generate_event_id()
310
336
  on_handle_tools_list_start_time = time.time()
311
- result = await call_next(context)
312
- self.logger.info(f"PROFILE: tools/list call_next duration: {time.time() - on_handle_tools_list_start_time:.2f} seconds id: {event_id}")
337
+
338
+ async with self._tools_list_lock:
339
+ if not self._tools_list_in_progress or self._tools_list_in_progress.done():
340
+ self._tools_list_in_progress = asyncio.create_task(call_next(context))
341
+ shared_task = self._tools_list_in_progress
342
+
343
+ try:
344
+ result = await shared_task
345
+ except Exception as e:
346
+ async with self._tools_list_lock:
347
+ if self._tools_list_in_progress is shared_task:
348
+ self._tools_list_in_progress = None
349
+ raise
350
+ self.logger.debug(
351
+ f"PROFILE: tools/list call_next duration: {time.time() - on_handle_tools_list_start_time:.2f} seconds id: {event_id}")
352
+
313
353
  tools_list = None
314
354
  if isinstance(result, list):
315
355
  tools_list = result
@@ -339,11 +379,13 @@ class SecurityMiddleware(Middleware):
339
379
  enhanced_result = result
340
380
 
341
381
  on_handle_tools_list_duration = time.time() - on_handle_tools_list_start_time
342
- self.logger.info(f"PROFILE: tools/list enhanced_result duration: {on_handle_tools_list_duration:.2f} seconds id: {event_id}")
382
+ self.logger.debug(
383
+ f"PROFILE: tools/list enhanced_result duration: {on_handle_tools_list_duration:.2f} seconds id: {event_id}")
343
384
  return enhanced_result
344
385
 
345
386
  on_handle_tools_list_duration = time.time() - on_handle_tools_list_start_time
346
- self.logger.info(f"PROFILE: tools/list result duration: {on_handle_tools_list_duration:.2f} seconds id: {event_id}")
387
+ self.logger.debug(
388
+ f"PROFILE: tools/list result duration: {on_handle_tools_list_duration:.2f} seconds id: {event_id}")
347
389
 
348
390
  return result
349
391
 
@@ -482,12 +524,12 @@ class SecurityMiddleware(Middleware):
482
524
  file_path_prefix = 'file://'
483
525
  if uri.startswith(file_path_prefix):
484
526
  path = urllib.parse.unquote(uri[len(file_path_prefix):])
485
-
527
+
486
528
  # Windows fix: remove leading slash before drive letter
487
529
  # file:///C:/path becomes /C:/path, should be C:/path
488
530
  if sys.platform == 'win32' and len(path) >= 3 and path[0] == '/' and path[2] == ':':
489
531
  path = path[1:]
490
-
532
+
491
533
  try:
492
534
  resolved_path = str(Path(path).resolve())
493
535
  workspace_roots.append(resolved_path)
@@ -614,7 +656,6 @@ class SecurityMiddleware(Middleware):
614
656
  # Don't fail the operation if API call fails - just log the error
615
657
  self.logger.error(f"Failed to record user confirmation: {e}")
616
658
 
617
-
618
659
  @staticmethod
619
660
  def _create_security_api_failure_decision(error: Exception) -> Dict[str, Any]:
620
661
  """Create a standard failure decision when security API is unavailable/failing/unreachable"""
@@ -722,7 +763,7 @@ class SecurityMiddleware(Middleware):
722
763
  error_parts = [
723
764
  f"SECURITY POLICY NEEDS MORE INFORMATION FOR REVIEWING {stage_title}:",
724
765
  '\n'.join(reasons),
725
- '' # newline
766
+ '' # newline
726
767
  ]
727
768
 
728
769
  if need_fields:
@@ -749,7 +790,6 @@ class SecurityMiddleware(Middleware):
749
790
  error_parts.append("MISSING INFORMATION:")
750
791
  error_parts.extend(need_fields)
751
792
 
752
-
753
793
  error_parts.append("\nMANDATORY ACTIONS:")
754
794
  error_parts.append("1. Add/Edit ALL affected fields according to the required information")
755
795
  error_parts.append("2. Retry the tool call")
@@ -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 = '"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
 
File without changes
File without changes
File without changes