mcpower-proxy 0.0.68__tar.gz → 0.0.70__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.68 → mcpower_proxy-0.0.70}/PKG-INFO +2 -2
  2. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/pyproject.toml +2 -2
  3. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/mcpower_proxy.egg-info/PKG-INFO +2 -2
  4. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/mcpower_proxy.egg-info/requires.txt +1 -1
  5. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/apis/security_policy.py +1 -1
  6. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/wrapper/__version__.py +1 -1
  7. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/wrapper/middleware.py +57 -21
  8. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/wrapper/server.py +19 -11
  9. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/LICENSE +0 -0
  10. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/README.md +0 -0
  11. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/setup.cfg +0 -0
  12. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/LICENSE +0 -0
  13. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/main.py +0 -0
  14. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/mcpower_proxy.egg-info/SOURCES.txt +0 -0
  15. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/mcpower_proxy.egg-info/dependency_links.txt +0 -0
  16. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/mcpower_proxy.egg-info/entry_points.txt +0 -0
  17. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/mcpower_proxy.egg-info/top_level.txt +0 -0
  18. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/__init__.py +0 -0
  19. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/apis/__init__.py +0 -0
  20. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/logs/__init__.py +0 -0
  21. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/logs/audit_trail.py +0 -0
  22. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/logs/logger.py +0 -0
  23. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/redaction/__init__.py +0 -0
  24. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/redaction/constants.py +0 -0
  25. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/redaction/gitleaks_rules.py +0 -0
  26. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/redaction/pii_rules.py +0 -0
  27. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/redaction/redactor.py +0 -0
  28. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/ui/__init__.py +0 -0
  29. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/ui/classes.py +0 -0
  30. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/ui/confirmation.py +0 -0
  31. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/ui/simple_dialog.py +0 -0
  32. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/ui/xdialog/__init__.py +0 -0
  33. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/ui/xdialog/constants.py +0 -0
  34. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/ui/xdialog/mac_dialogs.py +0 -0
  35. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/ui/xdialog/tk_dialogs.py +0 -0
  36. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/ui/xdialog/windows_custom_dialog.py +0 -0
  37. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/ui/xdialog/windows_dialogs.py +0 -0
  38. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/ui/xdialog/windows_structs.py +0 -0
  39. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/ui/xdialog/yad_dialogs.py +0 -0
  40. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/ui/xdialog/zenity_dialogs.py +0 -0
  41. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/utils/__init__.py +0 -0
  42. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/utils/cli.py +0 -0
  43. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/utils/config.py +0 -0
  44. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/utils/copy.py +0 -0
  45. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/utils/ids.py +0 -0
  46. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/utils/json.py +0 -0
  47. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/modules/utils/mcp_configs.py +0 -0
  48. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/src/wrapper/__init__.py +0 -0
  49. {mcpower_proxy-0.0.68 → mcpower_proxy-0.0.70}/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.68
3
+ Version: 0.0.70
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.68"
3
+ version = "0.0.70"
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.68
3
+ Version: 0.0.70
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
@@ -190,7 +190,7 @@ class SecurityPolicyClient:
190
190
  raise SecurityAPIError(f"Unsupported HTTP method: {method}. Supported methods: POST, PUT")
191
191
 
192
192
  on_make_request_duration = time.time() - on_make_request_start_time
193
- 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}")
194
194
 
195
195
  match response.status_code:
196
196
  case 200:
@@ -3,4 +3,4 @@
3
3
  Wrapper MCP Server Version
4
4
  """
5
5
 
6
- __version__ = "0.0.68"
6
+ __version__ = "0.0.70"
@@ -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,8 @@ 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
15
18
 
16
19
  from mcpower_shared.mcp_types import (create_policy_request, create_policy_response, AgentContext, EnvironmentContext,
17
20
  InitRequest,
@@ -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)
@@ -96,13 +99,28 @@ class SecurityMiddleware(Middleware):
96
99
  workspace_roots = await self._extract_workspace_roots(context)
97
100
  current_workspace_root = workspace_roots[0] if workspace_roots else str(Path.home() / ".mcpower")
98
101
  if current_workspace_root != self._last_workspace_root:
99
- self.logger.debug(f"Workspace root changed from {self._last_workspace_root} to {current_workspace_root}")
102
+ self.logger.debug(
103
+ f"Workspace root changed from {self._last_workspace_root} to {current_workspace_root}")
100
104
  self._last_workspace_root = current_workspace_root
101
105
  self.app_id = read_app_uid(logger=self.logger, project_folder_path=current_workspace_root)
102
106
  self.audit_logger.set_app_uid(self.app_id)
103
107
 
104
108
  operation_type = "message"
105
- 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
106
124
 
107
125
  match context.type:
108
126
  case "request":
@@ -119,13 +137,13 @@ class SecurityMiddleware(Middleware):
119
137
  operation_type = "prompt"
120
138
  case "tools/list":
121
139
  # Special handling for tools/list - call /init instead of normal inspection
122
- return await self._handle_tools_list(context, call_next)
140
+ return await self._handle_tools_list(context, call_next_wrapper)
123
141
  case "initialize" | "resources/list" | "resources/templates/list" | "prompts/list":
124
- return await call_next_callback(context)
142
+ return await call_next_wrapper(context)
125
143
 
126
144
  return await self._handle_operation(
127
145
  context=context,
128
- call_next=call_next_callback,
146
+ call_next=call_next_wrapper,
129
147
  error_class=FastMCPError,
130
148
  operation_type=operation_type
131
149
  )
@@ -185,7 +203,7 @@ class SecurityMiddleware(Middleware):
185
203
  return await ProxyClient.default_progress_handler(progress, total, message)
186
204
 
187
205
  async def secure_log_handler(self, log_message):
188
- # FIXME: log_message should be redacted before logging,
206
+ # FIXME: log_message should be redacted before logging,
189
207
  self.logger.info(f"secure_log_handler: {str(log_message)[:100]}...")
190
208
  # FIXME: log_message should be reviewed with policy before forwarding
191
209
 
@@ -226,7 +244,8 @@ class SecurityMiddleware(Middleware):
226
244
  prompt_id=prompt_id
227
245
  )
228
246
  on_inspect_request_duration = time.time() - on_inspect_request_start_time
229
- 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")
230
249
 
231
250
  await self._enforce_decision(
232
251
  decision=request_decision,
@@ -255,7 +274,8 @@ class SecurityMiddleware(Middleware):
255
274
  # Call wrapped MCP with cleaned context (e.g., no wrapper args)
256
275
  result = await call_next(cleaned_context)
257
276
  on_call_next_duration = time.time() - on_call_next_start_time
258
- 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")
259
279
 
260
280
  response_content = self._extract_response_content(result)
261
281
 
@@ -280,7 +300,8 @@ class SecurityMiddleware(Middleware):
280
300
  prompt_id=prompt_id
281
301
  )
282
302
  on_inspect_response_duration = time.time() - on_inspect_response_start_time
283
- 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")
284
305
 
285
306
  await self._enforce_decision(
286
307
  decision=response_decision,
@@ -305,15 +326,30 @@ class SecurityMiddleware(Middleware):
305
326
  prompt_id=prompt_id
306
327
  )
307
328
  on_handle_operation_duration = time.time() - on_handle_operation_start_time
308
- 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")
309
331
  return result
310
332
 
311
333
  async def _handle_tools_list(self, context: MiddlewareContext, call_next: CallNext) -> Any:
312
- """Handle tools/list by calling /init API and modifying schemas"""
334
+ """Handle tools/list by calling /init API and modifying schemas with deduplication"""
313
335
  event_id = generate_event_id()
314
336
  on_handle_tools_list_start_time = time.time()
315
- result = await call_next(context)
316
- 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
+
317
353
  tools_list = None
318
354
  if isinstance(result, list):
319
355
  tools_list = result
@@ -343,11 +379,13 @@ class SecurityMiddleware(Middleware):
343
379
  enhanced_result = result
344
380
 
345
381
  on_handle_tools_list_duration = time.time() - on_handle_tools_list_start_time
346
- 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}")
347
384
  return enhanced_result
348
385
 
349
386
  on_handle_tools_list_duration = time.time() - on_handle_tools_list_start_time
350
- 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}")
351
389
 
352
390
  return result
353
391
 
@@ -618,7 +656,6 @@ class SecurityMiddleware(Middleware):
618
656
  # Don't fail the operation if API call fails - just log the error
619
657
  self.logger.error(f"Failed to record user confirmation: {e}")
620
658
 
621
-
622
659
  @staticmethod
623
660
  def _create_security_api_failure_decision(error: Exception) -> Dict[str, Any]:
624
661
  """Create a standard failure decision when security API is unavailable/failing/unreachable"""
@@ -726,7 +763,7 @@ class SecurityMiddleware(Middleware):
726
763
  error_parts = [
727
764
  f"SECURITY POLICY NEEDS MORE INFORMATION FOR REVIEWING {stage_title}:",
728
765
  '\n'.join(reasons),
729
- '' # newline
766
+ '' # newline
730
767
  ]
731
768
 
732
769
  if need_fields:
@@ -753,7 +790,6 @@ class SecurityMiddleware(Middleware):
753
790
  error_parts.append("MISSING INFORMATION:")
754
791
  error_parts.extend(need_fields)
755
792
 
756
-
757
793
  error_parts.append("\nMANDATORY ACTIONS:")
758
794
  error_parts.append("1. Add/Edit ALL affected fields according to the required information")
759
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 = '"@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
 
File without changes
File without changes
File without changes