mcpower-proxy 0.0.74__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/common/hooks/init.py +13 -7
- ide_tools/common/hooks/prompt_submit.py +6 -3
- ide_tools/common/hooks/read_file.py +6 -3
- ide_tools/common/hooks/shell_execution.py +5 -3
- ide_tools/common/hooks/shell_parser_bashlex.py +182 -65
- ide_tools/cursor/constants.py +23 -4
- ide_tools/cursor/router.py +10 -3
- {mcpower_proxy-0.0.74.dist-info → mcpower_proxy-0.0.79.dist-info}/METADATA +2 -2
- {mcpower_proxy-0.0.74.dist-info → mcpower_proxy-0.0.79.dist-info}/RECORD +18 -16
- modules/logs/audit_trail.py +5 -4
- modules/utils/platform.py +23 -0
- modules/utils/string.py +17 -0
- wrapper/__version__.py +1 -1
- wrapper/middleware.py +9 -4
- {mcpower_proxy-0.0.74.dist-info → mcpower_proxy-0.0.79.dist-info}/WHEEL +0 -0
- {mcpower_proxy-0.0.74.dist-info → mcpower_proxy-0.0.79.dist-info}/entry_points.txt +0 -0
- {mcpower_proxy-0.0.74.dist-info → mcpower_proxy-0.0.79.dist-info}/licenses/LICENSE +0 -0
- {mcpower_proxy-0.0.74.dist-info → mcpower_proxy-0.0.79.dist-info}/top_level.txt +0 -0
ide_tools/common/hooks/init.py
CHANGED
|
@@ -36,6 +36,7 @@ async def handle_init(
|
|
|
36
36
|
logger: MCPLogger,
|
|
37
37
|
audit_logger: AuditTrailLogger,
|
|
38
38
|
event_id: str,
|
|
39
|
+
prompt_id: str,
|
|
39
40
|
cwd: Optional[str],
|
|
40
41
|
server_name: str,
|
|
41
42
|
client_name: str,
|
|
@@ -48,6 +49,7 @@ async def handle_init(
|
|
|
48
49
|
logger: Logger instance
|
|
49
50
|
audit_logger: Audit logger instance
|
|
50
51
|
event_id: Event identifier
|
|
52
|
+
prompt_id: Prompt identifier
|
|
51
53
|
cwd: Current working directory
|
|
52
54
|
server_name: IDE-specific server name (e.g. "cursor_tools_mcp")
|
|
53
55
|
client_name: IDE-specific client name (e.g. "cursor", "claude-code")
|
|
@@ -57,23 +59,27 @@ async def handle_init(
|
|
|
57
59
|
"""
|
|
58
60
|
session_id = get_session_id()
|
|
59
61
|
|
|
60
|
-
logger.info(f"Init handler started (client={client_name}, event_id={event_id}, cwd={cwd})")
|
|
62
|
+
logger.info(f"Init handler started (client={client_name}, event_id={event_id}, prompt_id={prompt_id}, cwd={cwd})")
|
|
61
63
|
|
|
62
64
|
try:
|
|
63
65
|
app_uid = read_app_uid(logger, get_project_mcpower_dir(cwd))
|
|
64
66
|
audit_logger.set_app_uid(app_uid)
|
|
65
67
|
|
|
66
|
-
audit_logger.log_event(
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
audit_logger.log_event(
|
|
69
|
+
"mcpower_start",
|
|
70
|
+
{
|
|
71
|
+
"wrapper_version": __version__,
|
|
72
|
+
"wrapped_server_name": server_name,
|
|
73
|
+
"client": client_name
|
|
74
|
+
}
|
|
75
|
+
)
|
|
71
76
|
|
|
72
77
|
try:
|
|
73
78
|
tools = [
|
|
74
79
|
ToolRef(
|
|
75
80
|
name=hook_info["name"],
|
|
76
|
-
description=hook_info[
|
|
81
|
+
description=f"Description:\n{hook_info['description']}\n\n"
|
|
82
|
+
f"inputSchema:\n{hook_info['parameters']}",
|
|
77
83
|
version=hook_info["version"]
|
|
78
84
|
)
|
|
79
85
|
for hook_info in hooks.values()
|
|
@@ -8,6 +8,7 @@ from modules.logs.audit_trail import AuditTrailLogger
|
|
|
8
8
|
from modules.logs.logger import MCPLogger
|
|
9
9
|
from modules.redaction import redact
|
|
10
10
|
from modules.utils.ids import get_session_id, read_app_uid, get_project_mcpower_dir
|
|
11
|
+
from modules.utils.string import truncate_at
|
|
11
12
|
from .output import output_result, output_error
|
|
12
13
|
from .types import HookConfig
|
|
13
14
|
from .utils import create_validator, extract_redaction_patterns, process_attachments_for_redaction, inspect_and_enforce
|
|
@@ -65,9 +66,10 @@ async def handle_prompt_submit(
|
|
|
65
66
|
{
|
|
66
67
|
"server": config.server_name,
|
|
67
68
|
"tool": tool_name,
|
|
68
|
-
"params": {"prompt":
|
|
69
|
+
"params": {"prompt": truncate_at(redacted_prompt, 100), "attachments_count": len(attachments)}
|
|
69
70
|
},
|
|
70
|
-
event_id=event_id
|
|
71
|
+
event_id=event_id,
|
|
72
|
+
prompt_id=prompt_id
|
|
71
73
|
)
|
|
72
74
|
|
|
73
75
|
prompt_patterns = extract_redaction_patterns(redacted_prompt)
|
|
@@ -110,7 +112,8 @@ async def handle_prompt_submit(
|
|
|
110
112
|
"tool": tool_name,
|
|
111
113
|
"params": {"redactions_found": has_any_redactions}
|
|
112
114
|
},
|
|
113
|
-
event_id=event_id
|
|
115
|
+
event_id=event_id,
|
|
116
|
+
prompt_id=prompt_id
|
|
114
117
|
)
|
|
115
118
|
|
|
116
119
|
reasons = decision.get("reasons", [])
|
|
@@ -65,7 +65,8 @@ async def handle_read_file(
|
|
|
65
65
|
"tool": tool_name,
|
|
66
66
|
"params": {"file_path": file_path, "attachments_count": len(attachments)}
|
|
67
67
|
},
|
|
68
|
-
event_id=event_id
|
|
68
|
+
event_id=event_id,
|
|
69
|
+
prompt_id=prompt_id
|
|
69
70
|
)
|
|
70
71
|
|
|
71
72
|
# Check content length - skip API if too large
|
|
@@ -84,7 +85,8 @@ async def handle_read_file(
|
|
|
84
85
|
"content_too_large": True
|
|
85
86
|
}
|
|
86
87
|
},
|
|
87
|
-
event_id=event_id
|
|
88
|
+
event_id=event_id,
|
|
89
|
+
prompt_id=prompt_id
|
|
88
90
|
)
|
|
89
91
|
|
|
90
92
|
output_result(logger, config.output_format, "permission", True)
|
|
@@ -145,7 +147,8 @@ async def handle_read_file(
|
|
|
145
147
|
"content_length": len(provided_content),
|
|
146
148
|
"attachments_with_redactions": len(files_with_redactions)}
|
|
147
149
|
},
|
|
148
|
-
event_id=event_id
|
|
150
|
+
event_id=event_id,
|
|
151
|
+
prompt_id=prompt_id
|
|
149
152
|
)
|
|
150
153
|
|
|
151
154
|
reasons = decision.get("reasons", [])
|
|
@@ -38,7 +38,7 @@ def extract_and_redact_command_files(
|
|
|
38
38
|
|
|
39
39
|
try:
|
|
40
40
|
# Parse command to extract input files
|
|
41
|
-
_, input_files = parse_shell_command(command)
|
|
41
|
+
_, input_files = parse_shell_command(command, initial_cwd=cwd)
|
|
42
42
|
|
|
43
43
|
logger.info(f"Extracted {len(input_files)} input files from command: {input_files}")
|
|
44
44
|
|
|
@@ -203,7 +203,8 @@ async def _handle_shell_operation(
|
|
|
203
203
|
audit_logger.log_event(
|
|
204
204
|
audit_event_type,
|
|
205
205
|
get_audit_data(),
|
|
206
|
-
event_id=event_id
|
|
206
|
+
event_id=event_id,
|
|
207
|
+
prompt_id=prompt_id
|
|
207
208
|
)
|
|
208
209
|
|
|
209
210
|
# Build content_data with redacted fields and files
|
|
@@ -230,7 +231,8 @@ async def _handle_shell_operation(
|
|
|
230
231
|
audit_logger.log_event(
|
|
231
232
|
audit_forwarded_event_type,
|
|
232
233
|
get_audit_data(),
|
|
233
|
-
event_id=event_id
|
|
234
|
+
event_id=event_id,
|
|
235
|
+
prompt_id=prompt_id
|
|
234
236
|
)
|
|
235
237
|
|
|
236
238
|
reasons = decision.get("reasons", [])
|
|
@@ -5,15 +5,17 @@ Parses shell commands to extract sub-commands and file references using proper b
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import bashlex
|
|
8
|
-
|
|
8
|
+
import os
|
|
9
|
+
from typing import List, Tuple, Set, Optional, Dict
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
def parse_shell_command(command: str) -> Tuple[List[str], List[str]]:
|
|
12
|
+
def parse_shell_command(command: str, initial_cwd: Optional[str] = None) -> Tuple[List[str], List[str]]:
|
|
12
13
|
"""
|
|
13
14
|
Parse a shell command using bashlex and extract sub-commands and input files.
|
|
14
15
|
|
|
15
16
|
Args:
|
|
16
17
|
command: A shell command string (supports pipes, redirections, etc.)
|
|
18
|
+
initial_cwd: Initial working directory (defaults to current directory)
|
|
17
19
|
|
|
18
20
|
Returns:
|
|
19
21
|
A tuple of (sub_commands, input_files) where:
|
|
@@ -43,8 +45,14 @@ def parse_shell_command(command: str) -> Tuple[List[str], List[str]]:
|
|
|
43
45
|
all_files: Set[str] = set()
|
|
44
46
|
output_files: Set[str] = set()
|
|
45
47
|
|
|
48
|
+
# Track directory changes
|
|
49
|
+
context = {
|
|
50
|
+
'cwd': initial_cwd or os.getcwd(),
|
|
51
|
+
'file_to_cwd': {} # Map each file to the directory it was found in
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
for ast in parts:
|
|
47
|
-
_extract_from_ast(ast, command, sub_commands, all_files, output_files)
|
|
55
|
+
_extract_from_ast(ast, command, sub_commands, all_files, output_files, False, context)
|
|
48
56
|
|
|
49
57
|
# Remove output-only files from the result
|
|
50
58
|
input_files = sorted(list(all_files - output_files))
|
|
@@ -58,7 +66,8 @@ def _extract_from_ast(
|
|
|
58
66
|
sub_commands: List[str],
|
|
59
67
|
all_files: Set[str],
|
|
60
68
|
output_files: Set[str],
|
|
61
|
-
parent_is_pipe: bool = False
|
|
69
|
+
parent_is_pipe: bool = False,
|
|
70
|
+
context: Optional[Dict] = None
|
|
62
71
|
) -> None:
|
|
63
72
|
"""
|
|
64
73
|
Recursively extract sub-commands and files from a bashlex AST node.
|
|
@@ -70,19 +79,24 @@ def _extract_from_ast(
|
|
|
70
79
|
all_files: Set to add all file references to
|
|
71
80
|
output_files: Set to add output-only files to
|
|
72
81
|
parent_is_pipe: True if parent node is a pipe operator
|
|
82
|
+
context: Dictionary with 'cwd' for current working directory
|
|
73
83
|
"""
|
|
84
|
+
if context is None:
|
|
85
|
+
context = {'cwd': os.getcwd()}
|
|
86
|
+
|
|
74
87
|
# Check node kind to determine type
|
|
75
88
|
node_kind = getattr(node, 'kind', None)
|
|
76
89
|
|
|
77
90
|
if node_kind == 'list':
|
|
78
|
-
# List node contains multiple parts connected by operators
|
|
91
|
+
# List node contains multiple parts connected by operators (&&, ||, ;)
|
|
92
|
+
# Process sequentially to track directory changes
|
|
79
93
|
if hasattr(node, 'parts'):
|
|
80
94
|
for part in node.parts:
|
|
81
|
-
_extract_from_ast(part, command, sub_commands, all_files, output_files, False)
|
|
95
|
+
_extract_from_ast(part, command, sub_commands, all_files, output_files, False, context)
|
|
82
96
|
|
|
83
97
|
elif node_kind == 'pipeline':
|
|
84
98
|
# Pipeline node - extract individual commands
|
|
85
|
-
_extract_pipeline(node, command, sub_commands, all_files, output_files)
|
|
99
|
+
_extract_pipeline(node, command, sub_commands, all_files, output_files, context)
|
|
86
100
|
|
|
87
101
|
elif node_kind == 'command':
|
|
88
102
|
# Command node - extract the command text and analyze its parts
|
|
@@ -91,20 +105,38 @@ def _extract_from_ast(
|
|
|
91
105
|
cmd_text = command[start:end]
|
|
92
106
|
sub_commands.append(cmd_text)
|
|
93
107
|
|
|
108
|
+
# Get the command name (first word) for context
|
|
109
|
+
cmd_name = None
|
|
110
|
+
if hasattr(node, 'parts') and len(node.parts) > 0:
|
|
111
|
+
first_part = node.parts[0]
|
|
112
|
+
if hasattr(first_part, 'word'):
|
|
113
|
+
cmd_name = first_part.word
|
|
114
|
+
|
|
115
|
+
# Check if this is a cd command and update context
|
|
116
|
+
if cmd_name == 'cd' and hasattr(node, 'parts') and len(node.parts) > 1:
|
|
117
|
+
second_part = node.parts[1]
|
|
118
|
+
if hasattr(second_part, 'word'):
|
|
119
|
+
target_dir = second_part.word
|
|
120
|
+
# Resolve the new directory
|
|
121
|
+
if os.path.isabs(target_dir):
|
|
122
|
+
context['cwd'] = target_dir
|
|
123
|
+
else:
|
|
124
|
+
context['cwd'] = os.path.normpath(os.path.join(context['cwd'], target_dir))
|
|
125
|
+
|
|
94
126
|
# Extract files from command parts (arguments and redirections)
|
|
95
127
|
if hasattr(node, 'parts'):
|
|
96
|
-
for part in node.parts:
|
|
128
|
+
for i, part in enumerate(node.parts):
|
|
97
129
|
part_kind = getattr(part, 'kind', None)
|
|
98
130
|
if part_kind == 'redirect':
|
|
99
|
-
_extract_redirect(part, command, all_files, output_files)
|
|
100
|
-
|
|
101
|
-
_extract_files_from_node(part, command, all_files, output_files)
|
|
131
|
+
_extract_redirect(part, command, all_files, output_files, context)
|
|
132
|
+
elif i > 0: # Skip the command name itself (index 0)
|
|
133
|
+
_extract_files_from_node(part, command, all_files, output_files, cmd_name, context)
|
|
102
134
|
|
|
103
135
|
elif node_kind == 'compound':
|
|
104
136
|
# Compound command (like if, while, for, etc.)
|
|
105
137
|
if hasattr(node, 'list'):
|
|
106
138
|
for item in node.list:
|
|
107
|
-
_extract_from_ast(item, command, sub_commands, all_files, output_files, False)
|
|
139
|
+
_extract_from_ast(item, command, sub_commands, all_files, output_files, False, context)
|
|
108
140
|
|
|
109
141
|
elif node_kind == 'operator':
|
|
110
142
|
# Operator node (like &&, ||, ;) - ignore
|
|
@@ -115,45 +147,62 @@ def _extract_from_ast(
|
|
|
115
147
|
pass
|
|
116
148
|
|
|
117
149
|
|
|
118
|
-
def _extract_pipeline(node, command: str, sub_commands: List[str], all_files: Set[str], output_files: Set[str]) -> None:
|
|
150
|
+
def _extract_pipeline(node, command: str, sub_commands: List[str], all_files: Set[str], output_files: Set[str], context: Dict) -> None:
|
|
119
151
|
"""Extract commands from a pipeline node."""
|
|
120
152
|
if hasattr(node, 'parts'):
|
|
121
153
|
for part in node.parts:
|
|
122
154
|
part_kind = getattr(part, 'kind', None)
|
|
123
155
|
# Skip pipe nodes, only process commands
|
|
124
156
|
if part_kind != 'pipe':
|
|
125
|
-
_extract_from_ast(part, command, sub_commands, all_files, output_files, True)
|
|
157
|
+
_extract_from_ast(part, command, sub_commands, all_files, output_files, True, context)
|
|
126
158
|
|
|
127
159
|
|
|
128
|
-
def _extract_files_from_node(node, command: str, all_files: Set[str], output_files: Set[str]) -> None:
|
|
129
|
-
"""Extract file references from a node.
|
|
160
|
+
def _extract_files_from_node(node, command: str, all_files: Set[str], output_files: Set[str], cmd_name: Optional[str] = None, context: Optional[Dict] = None) -> None:
|
|
161
|
+
"""Extract file references from a node.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
node: bashlex AST node
|
|
165
|
+
command: Original command string
|
|
166
|
+
all_files: Set to add all file references to
|
|
167
|
+
output_files: Set to add output-only files to
|
|
168
|
+
cmd_name: Name of the command this node belongs to (for context)
|
|
169
|
+
context: Dictionary with 'cwd' for current working directory
|
|
170
|
+
"""
|
|
171
|
+
if context is None:
|
|
172
|
+
context = {'cwd': os.getcwd()}
|
|
173
|
+
|
|
130
174
|
node_kind = getattr(node, 'kind', None)
|
|
131
175
|
|
|
132
176
|
if node_kind == 'word':
|
|
133
177
|
# Word node - check if it's a file reference
|
|
134
178
|
word = node.word if hasattr(node, 'word') else None
|
|
135
179
|
|
|
136
|
-
if word and _looks_like_file(word):
|
|
137
|
-
|
|
180
|
+
if word and _looks_like_file(word, cmd_name):
|
|
181
|
+
# Resolve relative paths against current working directory
|
|
182
|
+
resolved_path = _resolve_path(word, context['cwd'])
|
|
183
|
+
all_files.add(resolved_path)
|
|
138
184
|
|
|
139
185
|
# Recursively check parts (for command substitutions, etc.)
|
|
140
186
|
if hasattr(node, 'parts'):
|
|
141
187
|
for part in node.parts:
|
|
142
|
-
_extract_files_from_node(part, command, all_files, output_files)
|
|
188
|
+
_extract_files_from_node(part, command, all_files, output_files, cmd_name, context)
|
|
143
189
|
|
|
144
190
|
elif node_kind == 'commandsubstitution':
|
|
145
191
|
# Command substitution $(...) - recursively parse
|
|
146
192
|
if hasattr(node, 'command'):
|
|
147
|
-
_extract_from_ast(node.command, command, [], all_files, output_files, False)
|
|
193
|
+
_extract_from_ast(node.command, command, [], all_files, output_files, False, context)
|
|
148
194
|
|
|
149
195
|
elif node_kind == 'processsubstitution':
|
|
150
196
|
# Process substitution <(...) or >(...) - recursively parse
|
|
151
197
|
if hasattr(node, 'command'):
|
|
152
|
-
_extract_from_ast(node.command, command, [], all_files, output_files, False)
|
|
198
|
+
_extract_from_ast(node.command, command, [], all_files, output_files, False, context)
|
|
153
199
|
|
|
154
200
|
|
|
155
|
-
def _extract_redirect(redirect, command: str, all_files: Set[str], output_files: Set[str]) -> None:
|
|
201
|
+
def _extract_redirect(redirect, command: str, all_files: Set[str], output_files: Set[str], context: Optional[Dict] = None) -> None:
|
|
156
202
|
"""Extract file references from redirection nodes."""
|
|
203
|
+
if context is None:
|
|
204
|
+
context = {'cwd': os.getcwd()}
|
|
205
|
+
|
|
157
206
|
redirect_type = getattr(redirect, 'type', None)
|
|
158
207
|
|
|
159
208
|
# Get the target of the redirection
|
|
@@ -161,26 +210,49 @@ def _extract_redirect(redirect, command: str, all_files: Set[str], output_files:
|
|
|
161
210
|
target = redirect.output
|
|
162
211
|
target_word = target.word if hasattr(target, 'word') else None
|
|
163
212
|
|
|
164
|
-
|
|
213
|
+
# Redirections always point to files, not directories
|
|
214
|
+
if target_word and _looks_like_file(target_word, None):
|
|
215
|
+
# Resolve relative paths against current working directory
|
|
216
|
+
resolved_path = _resolve_path(target_word, context['cwd'])
|
|
217
|
+
|
|
165
218
|
# Determine if it's input or output
|
|
166
219
|
if redirect_type in ('>', '>>', '>&', '>|', '&>'):
|
|
167
220
|
# Output redirection
|
|
168
|
-
output_files.add(
|
|
169
|
-
all_files.add(
|
|
221
|
+
output_files.add(resolved_path)
|
|
222
|
+
all_files.add(resolved_path)
|
|
170
223
|
elif redirect_type == '<':
|
|
171
224
|
# Input redirection
|
|
172
|
-
all_files.add(
|
|
225
|
+
all_files.add(resolved_path)
|
|
173
226
|
else:
|
|
174
227
|
# Unknown, be conservative and include it
|
|
175
|
-
all_files.add(
|
|
228
|
+
all_files.add(resolved_path)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _resolve_path(path: str, cwd: str) -> str:
|
|
232
|
+
"""
|
|
233
|
+
Resolve a file path relative to a working directory.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
path: File path (relative or absolute)
|
|
237
|
+
cwd: Current working directory
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Absolute path
|
|
241
|
+
"""
|
|
242
|
+
if os.path.isabs(path):
|
|
243
|
+
return path
|
|
244
|
+
else:
|
|
245
|
+
return os.path.normpath(os.path.join(cwd, path))
|
|
176
246
|
|
|
177
247
|
|
|
178
|
-
def _looks_like_file(word: str) -> bool:
|
|
248
|
+
def _looks_like_file(word: str, cmd_name: Optional[str] = None) -> bool:
|
|
179
249
|
"""
|
|
180
|
-
Heuristic to determine if a word
|
|
250
|
+
Heuristic to determine if a word is an actual readable file path.
|
|
251
|
+
Not patterns, not variables, not directories - actual files we can open.
|
|
181
252
|
|
|
182
253
|
Args:
|
|
183
254
|
word: A word from the command
|
|
255
|
+
cmd_name: The command this word belongs to (for context)
|
|
184
256
|
|
|
185
257
|
Returns:
|
|
186
258
|
True if it looks like a file path
|
|
@@ -188,65 +260,109 @@ def _looks_like_file(word: str) -> bool:
|
|
|
188
260
|
if not word:
|
|
189
261
|
return False
|
|
190
262
|
|
|
191
|
-
#
|
|
263
|
+
# Commands that take directory arguments, not files
|
|
264
|
+
DIRECTORY_COMMANDS = {
|
|
265
|
+
'cd', 'pushd', 'popd', 'mkdir', 'rmdir', 'chdir',
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# If this is a directory command, reject all arguments
|
|
269
|
+
if cmd_name and cmd_name in DIRECTORY_COMMANDS:
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
# Exclude URLs (http://, https://, ftp://, file://, etc.)
|
|
273
|
+
if '://' in word:
|
|
274
|
+
return False
|
|
192
275
|
|
|
193
|
-
# Exclude shell
|
|
194
|
-
if word
|
|
276
|
+
# Exclude shell meta-characters and patterns
|
|
277
|
+
if any(char in word for char in ['*', '?', '[', ']']): # Glob patterns
|
|
195
278
|
return False
|
|
196
279
|
|
|
197
|
-
|
|
198
|
-
if word.startswith('$') or '${' in word or '$(' in word:
|
|
280
|
+
if '$' in word or '`' in word: # Variables or command substitution
|
|
199
281
|
return False
|
|
200
282
|
|
|
201
|
-
# Exclude sed/awk patterns
|
|
283
|
+
# Exclude sed/awk patterns
|
|
202
284
|
if word.startswith('s/') and word.count('/') >= 2:
|
|
203
285
|
return False
|
|
204
286
|
|
|
205
|
-
# Exclude regex patterns
|
|
206
|
-
if
|
|
287
|
+
# Exclude regex patterns
|
|
288
|
+
if word.startswith('^') or word.endswith('$'):
|
|
207
289
|
return False
|
|
208
290
|
|
|
209
|
-
# Exclude
|
|
210
|
-
if word.startswith('-') or word.startswith('+')
|
|
291
|
+
# Exclude options
|
|
292
|
+
if word.startswith('-') or word.startswith('+'):
|
|
211
293
|
return False
|
|
212
294
|
|
|
213
|
-
# Exclude
|
|
295
|
+
# Exclude bare dots
|
|
214
296
|
if word in {'.', '..'}:
|
|
215
297
|
return False
|
|
216
298
|
|
|
217
|
-
# Exclude
|
|
218
|
-
|
|
219
|
-
if word in {'/tmp', '/dev', '/usr', '/etc', '/var', '/opt', '/home'}:
|
|
299
|
+
# Exclude bare directories (but /tmp/file is OK)
|
|
300
|
+
if word in {'/', '/tmp', '/dev', '/usr', '/etc', '/var', '/opt', '/home'}:
|
|
220
301
|
return False
|
|
221
302
|
|
|
222
|
-
#
|
|
223
|
-
# Has an extension (but not just an extension)
|
|
224
|
-
if '.' in word and not word.startswith('.') and len(word) > 3:
|
|
225
|
-
if not word.startswith('*'):
|
|
226
|
-
# Make sure the extension looks reasonable (2-4 chars)
|
|
227
|
-
parts = word.rsplit('.', 1)
|
|
228
|
-
if len(parts) == 2 and 1 <= len(parts[1]) <= 4 and parts[1].isalnum():
|
|
229
|
-
return True
|
|
303
|
+
# --- POSITIVE CHECKS ---
|
|
230
304
|
|
|
231
|
-
# Has
|
|
305
|
+
# Has extension = very likely a file
|
|
306
|
+
if '.' in word and not word.startswith('.'):
|
|
307
|
+
# Get the extension
|
|
308
|
+
parts = word.rsplit('.', 1)
|
|
309
|
+
if len(parts) == 2:
|
|
310
|
+
name, ext = parts
|
|
311
|
+
# Be more permissive with extensions
|
|
312
|
+
if name and ext and ext.replace('_', '').replace('-', '').isalnum():
|
|
313
|
+
if len(ext) <= 10: # Most extensions are < 10 chars
|
|
314
|
+
return True
|
|
315
|
+
|
|
316
|
+
# Has path separator = could be a file
|
|
232
317
|
if '/' in word:
|
|
233
|
-
|
|
234
|
-
if
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
318
|
+
# Check if it's a path to something specific (not just dirs)
|
|
319
|
+
if not word.endswith('/'): # Not ending with / (directory indicator)
|
|
320
|
+
parts = word.split('/')
|
|
321
|
+
last_part = parts[-1] if parts else ''
|
|
322
|
+
|
|
323
|
+
# If last part has extension, definitely a file
|
|
324
|
+
if '.' in last_part and not last_part.startswith('.'):
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
# If it's under specific directories that contain files
|
|
328
|
+
if word.startswith('/dev/') and len(word) > 5: # /dev/null, /dev/tty, etc.
|
|
329
|
+
return True
|
|
330
|
+
if word.startswith('/tmp/') and len(word) > 5: # /tmp/anything
|
|
331
|
+
return True
|
|
332
|
+
if word.startswith('/etc/') and len(word) > 5: # /etc/passwd, etc.
|
|
333
|
+
return True
|
|
334
|
+
if word.startswith('/usr/bin/') and len(word) > 9: # Executables
|
|
335
|
+
return True
|
|
336
|
+
if word.startswith('/usr/local/bin/') and len(word) > 15:
|
|
337
|
+
return True
|
|
338
|
+
|
|
339
|
+
# If last part looks like a filename (even without extension)
|
|
340
|
+
if last_part and last_part.replace('-', '').replace('_', '').isalnum():
|
|
341
|
+
# Could be an executable or script
|
|
238
342
|
return True
|
|
239
343
|
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
344
|
+
# Check for well-known files without extensions (case-insensitive)
|
|
345
|
+
filename_only = word.split('/')[-1].lower()
|
|
346
|
+
if filename_only in {'makefile', 'readme', 'license', 'dockerfile',
|
|
347
|
+
'gemfile', 'rakefile', 'procfile', 'vagrantfile',
|
|
348
|
+
'jenkinsfile', 'cakefile', 'gulpfile', 'gruntfile',
|
|
349
|
+
'brewfile', 'berksfile', 'guardfile', 'fastfile',
|
|
350
|
+
'cartfile', 'appfile', 'podfile', 'snapfile'}:
|
|
244
351
|
return True
|
|
245
352
|
|
|
246
|
-
#
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
353
|
+
# Stand-alone word without path - be conservative
|
|
354
|
+
if '/' not in word:
|
|
355
|
+
# If it has an extension, probably a file in current directory
|
|
356
|
+
if '.' in word and not word.startswith('.'):
|
|
357
|
+
return True
|
|
358
|
+
|
|
359
|
+
# Well-known executable names without extensions
|
|
360
|
+
if word in {'script', 'run', 'build', 'test', 'deploy', 'install',
|
|
361
|
+
'configure', 'setup', 'bootstrap', 'init'}:
|
|
362
|
+
return True
|
|
363
|
+
|
|
364
|
+
# Otherwise, we can't be sure it's a file (could be a command)
|
|
365
|
+
return False
|
|
250
366
|
|
|
251
367
|
return False
|
|
252
368
|
|
|
@@ -255,6 +371,7 @@ def _looks_like_file(word: str) -> bool:
|
|
|
255
371
|
if __name__ == "__main__":
|
|
256
372
|
# Test cases
|
|
257
373
|
test_cases = [
|
|
374
|
+
"cd /Users/user/src/project/server && python test.py",
|
|
258
375
|
"python a.py | tee b.log",
|
|
259
376
|
"cat a.txt > /tmp/b.txt",
|
|
260
377
|
"grep foo file.txt | sort | uniq > output.txt",
|
ide_tools/cursor/constants.py
CHANGED
|
@@ -35,24 +35,43 @@ CURSOR_HOOKS = {
|
|
|
35
35
|
"name": "beforeShellExecution",
|
|
36
36
|
"description": "Triggered before a shell command is executed by the agent. "
|
|
37
37
|
"Allows inspection and potential blocking of shell commands.",
|
|
38
|
-
"version": "1.0.0"
|
|
38
|
+
"version": "1.0.0",
|
|
39
|
+
"parameters": '{"type":"object","properties":{"command":{"type":"string","description":"Full terminal '
|
|
40
|
+
'command"},"cwd":{"type":"string","description":"Current working directory"}},"required":['
|
|
41
|
+
'"command","cwd"],"additionalProperties":false}'
|
|
39
42
|
},
|
|
40
43
|
"afterShellExecution": {
|
|
41
44
|
"name": "afterShellExecution",
|
|
42
45
|
"description": "Triggered after a shell command completes execution. "
|
|
43
46
|
"Provides access to command output and exit status.",
|
|
44
|
-
"version": "1.0.0"
|
|
47
|
+
"version": "1.0.0",
|
|
48
|
+
"parameters": '{"type":"object","properties":{"command":{"type":"string","description":"Full terminal '
|
|
49
|
+
'command"},"output":{"type":"string","description":"Full terminal output"}},"required":['
|
|
50
|
+
'"command","output"],"additionalProperties":false}'
|
|
45
51
|
},
|
|
46
52
|
"beforeReadFile": {
|
|
47
53
|
"name": "beforeReadFile",
|
|
48
54
|
"description": "Triggered before the agent reads a file. "
|
|
49
55
|
"Allows inspection and potential blocking of file read operations.",
|
|
50
|
-
"version": "1.0.0"
|
|
56
|
+
"version": "1.0.0",
|
|
57
|
+
"parameters": '{"type":"object","properties":{"file_path":{"type":"string","description":"Absolute path to '
|
|
58
|
+
'the file being read"},"content":{"type":"string","description":"File contents"},'
|
|
59
|
+
'"attachments":{"type":"array","description":"Additional related attachments",'
|
|
60
|
+
'"items":{"type":"object","properties":{"type":{"type":"string","description":"Attachment '
|
|
61
|
+
'type"},"file_path":{"type":"string","description":"Absolute path to the attachment"}},'
|
|
62
|
+
'"required":["type","file_path"],"additionalProperties":false}}},"required":["file_path",'
|
|
63
|
+
'"content"],"additionalProperties":false}'
|
|
51
64
|
},
|
|
52
65
|
"beforeSubmitPrompt": {
|
|
53
66
|
"name": "beforeSubmitPrompt",
|
|
54
67
|
"description": "Triggered before a prompt is submitted to the AI model. "
|
|
55
68
|
"Allows inspection and modification of prompts.",
|
|
56
|
-
"version": "1.0.0"
|
|
69
|
+
"version": "1.0.0",
|
|
70
|
+
"parameters": '{"type":"object","properties":{"prompt":{"type":"string","description":"User prompt text"},'
|
|
71
|
+
'"attachments":{"type":"array","description":"Attachments associated with the prompt",'
|
|
72
|
+
'"items":{"type":"object","properties":{"type":{"type":"string","enum":["file","rule"],'
|
|
73
|
+
'"description":"Attachment type"},"filePath":{"type":"string","description":"Absolute path to '
|
|
74
|
+
'the attached file or rule"}},"required":["type","filePath"],"additionalProperties":false}}},'
|
|
75
|
+
'"required":["prompt"],"additionalProperties":false}'
|
|
57
76
|
}
|
|
58
77
|
}
|
ide_tools/cursor/router.py
CHANGED
|
@@ -16,6 +16,9 @@ from ide_tools.common.hooks.shell_execution import handle_shell_execution
|
|
|
16
16
|
from modules.logs.audit_trail import AuditTrailLogger
|
|
17
17
|
from modules.logs.logger import MCPLogger
|
|
18
18
|
from .constants import CURSOR_HOOKS, CURSOR_CONFIG
|
|
19
|
+
from ..common.hooks.output import output_result
|
|
20
|
+
|
|
21
|
+
MASK_AFTER_SHELL_EXEC = True
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def route_cursor_hook(logger: MCPLogger, audit_logger: AuditTrailLogger, stdin_input: str):
|
|
@@ -68,6 +71,7 @@ def route_cursor_hook(logger: MCPLogger, audit_logger: AuditTrailLogger, stdin_i
|
|
|
68
71
|
logger=logger,
|
|
69
72
|
audit_logger=audit_logger,
|
|
70
73
|
event_id=event_id,
|
|
74
|
+
prompt_id=prompt_id,
|
|
71
75
|
cwd=cwd,
|
|
72
76
|
server_name=CURSOR_CONFIG.server_name,
|
|
73
77
|
client_name="cursor",
|
|
@@ -78,9 +82,12 @@ def route_cursor_hook(logger: MCPLogger, audit_logger: AuditTrailLogger, stdin_i
|
|
|
78
82
|
handle_shell_execution(logger, audit_logger, stdin_input, prompt_id, event_id, cwd, CURSOR_CONFIG,
|
|
79
83
|
hook_event_name, is_request=True))
|
|
80
84
|
elif hook_event_name == "afterShellExecution":
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
if not MASK_AFTER_SHELL_EXEC:
|
|
86
|
+
asyncio.run(
|
|
87
|
+
handle_shell_execution(logger, audit_logger, stdin_input, prompt_id, event_id, cwd, CURSOR_CONFIG,
|
|
88
|
+
hook_event_name, is_request=False))
|
|
89
|
+
else:
|
|
90
|
+
output_result(logger, CURSOR_CONFIG.output_format, "permission", True, "", "")
|
|
84
91
|
elif hook_event_name == "beforeReadFile":
|
|
85
92
|
asyncio.run(handle_read_file(logger, audit_logger, stdin_input, prompt_id, event_id, cwd, CURSOR_CONFIG,
|
|
86
93
|
hook_event_name))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcpower-proxy
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.79
|
|
4
4
|
Summary: MCPower Security proxy
|
|
5
5
|
Author-email: MCPower Security <support@mcpower.tech>
|
|
6
6
|
License: Apache License
|
|
@@ -216,7 +216,7 @@ Requires-Dist: watchdog>=3.0.0
|
|
|
216
216
|
Requires-Dist: jsonc-parser>=1.1.5
|
|
217
217
|
Requires-Dist: jsonpath-ng>=1.7.0
|
|
218
218
|
Requires-Dist: pydantic>=2.8.0
|
|
219
|
-
Requires-Dist: mcpower-shared==0.1.
|
|
219
|
+
Requires-Dist: mcpower-shared==0.1.5
|
|
220
220
|
Requires-Dist: bashlex>=0.18
|
|
221
221
|
Dynamic: license-file
|
|
222
222
|
|
|
@@ -3,25 +3,25 @@ ide_tools/__init__.py,sha256=odLisqEeKVuZupQoeM-uP9WgQseVTYQG0eP2hhwUyAc,250
|
|
|
3
3
|
ide_tools/router.py,sha256=PbGz1cwUXR3Bcy879W2wgmT4f2uThXiXevpWSxcgKZQ,1538
|
|
4
4
|
ide_tools/common/__init__.py,sha256=cW2cN5y2l4PHGkkWtYgydrtXtmLU_UipJAf_c0VMTk0,65
|
|
5
5
|
ide_tools/common/hooks/__init__.py,sha256=O-waaJU1OQFxplrLZymSSiIyLDNlGlki6nX-nbJiqz0,61
|
|
6
|
-
ide_tools/common/hooks/init.py,sha256=
|
|
6
|
+
ide_tools/common/hooks/init.py,sha256=N_MaDGE92qqLaMlqOoavQ2EHWM1MzMudXnO6nmHpGQE,4215
|
|
7
7
|
ide_tools/common/hooks/output.py,sha256=FBezJL1S5vzLJWW_cLht4XFLDJvhV_w-UTqCQOLEusE,1879
|
|
8
|
-
ide_tools/common/hooks/prompt_submit.py,sha256=
|
|
9
|
-
ide_tools/common/hooks/read_file.py,sha256=
|
|
10
|
-
ide_tools/common/hooks/shell_execution.py,sha256=
|
|
11
|
-
ide_tools/common/hooks/shell_parser_bashlex.py,sha256=
|
|
8
|
+
ide_tools/common/hooks/prompt_submit.py,sha256=_FysGrlJXcblex-OapWVIv1bedP1IKR2d1cvkINU0uM,4907
|
|
9
|
+
ide_tools/common/hooks/read_file.py,sha256=FBHTxOXrQWlw2qtddAq4gfv5h1g6rTbQ-0uqqL9wXE0,6456
|
|
10
|
+
ide_tools/common/hooks/shell_execution.py,sha256=KbJSGLW__bPso45eAlbV13nnkKYwwd7-KodZ6VyUnS0,9522
|
|
11
|
+
ide_tools/common/hooks/shell_parser_bashlex.py,sha256=ePit2jduHvBYkmEvcslpjCGJ5H-amVEm1We1vVCmMNc,15002
|
|
12
12
|
ide_tools/common/hooks/types.py,sha256=ndisZ8YoasQ3HlVzzyLmpiBCQJs43YPmixbaIIxRpZM,1023
|
|
13
13
|
ide_tools/common/hooks/utils.py,sha256=7Q95IZZG_lShq5Jvw78M7QN5-tVNVeFzLU33qB8boEU,9425
|
|
14
14
|
ide_tools/cursor/__init__.py,sha256=YW_V8m0A0bou0wQW_wy3nt2L_7MaNWeNKYBx-NQkilw,153
|
|
15
|
-
ide_tools/cursor/constants.py,sha256=
|
|
15
|
+
ide_tools/cursor/constants.py,sha256=xpJXSFgFhTBaznrBj53RZU0WTBSrL884Cnc9W2WDRb4,3778
|
|
16
16
|
ide_tools/cursor/format.py,sha256=Lh-KH1IlsLL-0B98Bz-ywxpwXL8urK7yPteZGBTPOiY,972
|
|
17
|
-
ide_tools/cursor/router.py,sha256=
|
|
18
|
-
mcpower_proxy-0.0.
|
|
17
|
+
ide_tools/cursor/router.py,sha256=2M7RIGv_8sAPHy-wBoNLjnY45-YOE4ZRklLQjTxIGwM,4138
|
|
18
|
+
mcpower_proxy-0.0.79.dist-info/licenses/LICENSE,sha256=U6WUzdnBrbmVxBmY75ikW-KtinwYnowZ7yNb5hECrvY,11337
|
|
19
19
|
modules/__init__.py,sha256=mJglXQwSRhU-bBv4LXgfu7NfGN9K4BeQWMPApen5rAA,30
|
|
20
20
|
modules/decision_handler.py,sha256=P8isKzf4GIWz9SK-VJPtO8VJEgNp7rAIcVZngnaLHmw,9574
|
|
21
21
|
modules/apis/__init__.py,sha256=Y5WZpKJzHpnRJebk0F80ZRTjR2PpA2LlYLgqI3XlmRo,15
|
|
22
22
|
modules/apis/security_policy.py,sha256=3fljaTpzfh_Nj0-jVvbnCJBL-CZxvqFNWfSlrAawsBc,15103
|
|
23
23
|
modules/logs/__init__.py,sha256=dpboUQjuO02z8K-liCbm2DYkCa-CB_ZDV9WSSjNm7Fs,15
|
|
24
|
-
modules/logs/audit_trail.py,sha256=
|
|
24
|
+
modules/logs/audit_trail.py,sha256=yoxzvRDI8ldjC-5o9__PkvaAbM33jWbq_8Sm6uKj8o8,6198
|
|
25
25
|
modules/logs/logger.py,sha256=MJS0P8VEzUX-5udzQitznaBPCBAcZJCygUgwaDWSq94,4087
|
|
26
26
|
modules/redaction/__init__.py,sha256=e5NTmp-zonUdzzscih-w_WQ-X8Nvb8CE8b_d6SbrwWg,316
|
|
27
27
|
modules/redaction/constants.py,sha256=xbDSX8n72FuJu6JJ_sbBE0f5OcWuwEwHxBZuK9Xz-TI,1213
|
|
@@ -48,13 +48,15 @@ modules/utils/copy.py,sha256=9OJIqWn8PxPZXr3DTt_01jp0YgmPimckab1969WFh0c,1075
|
|
|
48
48
|
modules/utils/ids.py,sha256=rhhRz7RmFjuJGYLft1erHz7vJII1DRpr3iHxBhhFl1s,5743
|
|
49
49
|
modules/utils/json.py,sha256=OA-JtSBqh9qd1yfm-iyOefNBMH3ITFUdxAkj7O_JZ-Y,4024
|
|
50
50
|
modules/utils/mcp_configs.py,sha256=DZaujZnF9LlPDJHzyepH7fWSt1GTr-FEmShPCqnZ5aI,1829
|
|
51
|
+
modules/utils/platform.py,sha256=Kz1Dh_UkvMbfXqiyZIEj7INYaWE9wT6nM9WIKXM91uM,552
|
|
52
|
+
modules/utils/string.py,sha256=cOuwWReyBwOgjxtTRPq1R6pCpfkgfqVmjdiaDPufIZU,456
|
|
51
53
|
wrapper/__init__.py,sha256=OJUsuWSoN1JqIHq4bSrzuL7ufcYJcwAmYCrJjLH44LM,22
|
|
52
|
-
wrapper/__version__.py,sha256=
|
|
53
|
-
wrapper/middleware.py,sha256=
|
|
54
|
+
wrapper/__version__.py,sha256=98JsmtG5HIwyXB6YPi_uPCHgtqCJD3P7KsFa8zkg6uQ,82
|
|
55
|
+
wrapper/middleware.py,sha256=6FmmyTnyZ61LZgsSCapSOluNX3GwIhyatiRPkPaT5tk,30355
|
|
54
56
|
wrapper/schema.py,sha256=O-CtKI9eJ4eEnqeUXPCrK7QJAFJrdp_cFbmMyg452Aw,7952
|
|
55
57
|
wrapper/server.py,sha256=zoIW_bqXV9vKZNFtD-ij0X_LtKJEjucda6lQI5mU6qY,3440
|
|
56
|
-
mcpower_proxy-0.0.
|
|
57
|
-
mcpower_proxy-0.0.
|
|
58
|
-
mcpower_proxy-0.0.
|
|
59
|
-
mcpower_proxy-0.0.
|
|
60
|
-
mcpower_proxy-0.0.
|
|
58
|
+
mcpower_proxy-0.0.79.dist-info/METADATA,sha256=eyGyx3ZiPMCYH2LEu4IiTuJEKXLGXnAlxu1zw5SCKRo,15698
|
|
59
|
+
mcpower_proxy-0.0.79.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
60
|
+
mcpower_proxy-0.0.79.dist-info/entry_points.txt,sha256=0smL8dxE7ERNz6XEggNaUC3QzKp8mD-v4q5nVEo0MXE,48
|
|
61
|
+
mcpower_proxy-0.0.79.dist-info/top_level.txt,sha256=OcCYMHHqbZq3mP5IZDRGdHUNOsKhT1XVnk_mDF_49Es,31
|
|
62
|
+
mcpower_proxy-0.0.79.dist-info/RECORD,,
|
modules/logs/audit_trail.py
CHANGED
|
@@ -53,6 +53,7 @@ class AuditTrailLogger:
|
|
|
53
53
|
self,
|
|
54
54
|
event_type: str,
|
|
55
55
|
data: Dict[str, Any],
|
|
56
|
+
*,
|
|
56
57
|
event_id: Optional[str] = None,
|
|
57
58
|
prompt_id: Optional[str] = None,
|
|
58
59
|
user_prompt: Optional[str] = None,
|
|
@@ -88,14 +89,14 @@ class AuditTrailLogger:
|
|
|
88
89
|
if prompt_id:
|
|
89
90
|
event["prompt_id"] = prompt_id
|
|
90
91
|
|
|
91
|
-
# Include user_prompt text if provided (only needed once per prompt_id)
|
|
92
|
-
if user_prompt:
|
|
93
|
-
event["user_prompt"] = user_prompt
|
|
94
|
-
|
|
95
92
|
# Include event_id if provided (for pairing request/response)
|
|
96
93
|
if event_id:
|
|
97
94
|
event["event_id"] = event_id
|
|
98
95
|
|
|
96
|
+
# Include user_prompt text if provided (only needed once per prompt_id)
|
|
97
|
+
if user_prompt:
|
|
98
|
+
event["user_prompt"] = user_prompt
|
|
99
|
+
|
|
99
100
|
# If app_uid not set yet, queue the log
|
|
100
101
|
if self.app_uid is None:
|
|
101
102
|
self._pending_logs.append(event)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Platform detection utilities"""
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Optional, Literal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_client_os() -> Optional[Literal["macos", "windows", "linux"]]:
|
|
7
|
+
"""
|
|
8
|
+
Fetch Python's sys.platform and convert to standardized OS names.
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
"macos", "windows", "linux", or None if platform is unknown
|
|
12
|
+
"""
|
|
13
|
+
platform = sys.platform
|
|
14
|
+
|
|
15
|
+
if platform == "darwin":
|
|
16
|
+
return "macos"
|
|
17
|
+
elif platform == "win32":
|
|
18
|
+
return "windows"
|
|
19
|
+
elif platform == "linux":
|
|
20
|
+
return "linux"
|
|
21
|
+
else:
|
|
22
|
+
return None
|
|
23
|
+
|
modules/utils/string.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""String utility functions"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def truncate_at(text: str, max_length: int) -> str:
|
|
5
|
+
"""
|
|
6
|
+
Truncate string at max_length, appending '...' only if truncated.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
text: String to truncate
|
|
10
|
+
max_length: Maximum length before truncation
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Truncated string with '...' suffix if truncated, original if not
|
|
14
|
+
"""
|
|
15
|
+
if len(text) <= max_length:
|
|
16
|
+
return text
|
|
17
|
+
return f"{text[:max_length]}..."
|
wrapper/__version__.py
CHANGED
wrapper/middleware.py
CHANGED
|
@@ -28,6 +28,8 @@ from modules.utils.copy import safe_copy
|
|
|
28
28
|
from modules.utils.ids import generate_event_id, get_session_id, read_app_uid, get_project_mcpower_dir
|
|
29
29
|
from modules.utils.json import safe_json_dumps, to_dict
|
|
30
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
|
|
31
33
|
from wrapper.schema import merge_input_schema_with_existing
|
|
32
34
|
|
|
33
35
|
|
|
@@ -173,7 +175,7 @@ class SecurityMiddleware(Middleware):
|
|
|
173
175
|
async def secure_elicitation_handler(self, message, response_type, params, context):
|
|
174
176
|
# FIXME: elicitation message, params, and context should be redacted before logging
|
|
175
177
|
self.logger.info(f"secure_elicitation_handler: "
|
|
176
|
-
f"message={str(message)
|
|
178
|
+
f"message={truncate_at(str(message), 100)}, response_type={response_type},"
|
|
177
179
|
f"params={params}, context={context}")
|
|
178
180
|
|
|
179
181
|
mock_context = MockContext(
|
|
@@ -203,7 +205,7 @@ class SecurityMiddleware(Middleware):
|
|
|
203
205
|
|
|
204
206
|
async def secure_log_handler(self, log_message):
|
|
205
207
|
# FIXME: log_message should be redacted before logging,
|
|
206
|
-
self.logger.info(f"secure_log_handler: {str(log_message)
|
|
208
|
+
self.logger.info(f"secure_log_handler: {truncate_at(str(log_message), 100)}")
|
|
207
209
|
# FIXME: log_message should be reviewed with policy before forwarding
|
|
208
210
|
|
|
209
211
|
# Handle case where log_message.data is a string instead of dict
|
|
@@ -431,7 +433,8 @@ class SecurityMiddleware(Middleware):
|
|
|
431
433
|
for tool in tools:
|
|
432
434
|
tool_ref = ToolRef(
|
|
433
435
|
name=getattr(tool, 'name', 'unknown'),
|
|
434
|
-
description=getattr(tool, 'description', '')
|
|
436
|
+
description=f"Description:\n{getattr(tool, 'description', '')}\n\n"
|
|
437
|
+
f"inputSchema:\n{safe_json_dumps(getattr(tool, 'parameters', {}))}",
|
|
435
438
|
version=getattr(tool, 'version', None)
|
|
436
439
|
)
|
|
437
440
|
tool_refs.append(tool_ref)
|
|
@@ -654,7 +657,9 @@ class SecurityMiddleware(Middleware):
|
|
|
654
657
|
"current_files": wrapper_args.get('__wrapper_currentFiles')
|
|
655
658
|
},
|
|
656
659
|
client=self.wrapper_server_name,
|
|
657
|
-
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,
|
|
658
663
|
)
|
|
659
664
|
}
|
|
660
665
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|