Skip to content

Commit e128b19

Browse files
committed
Keep run_unix_command output in files for future use.
1 parent d4a17fc commit e128b19

File tree

4 files changed

+91
-27
lines changed

4 files changed

+91
-27
lines changed

mcp_service.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import sys
99

1010
import torch
11+
from starlette.middleware.cors import CORSMiddleware
1112
from uvicorn import Config, Server
1213

1314
from shyhurricane.config import configure
@@ -96,6 +97,17 @@ async def main():
9697
mcp_app = mcp_instance.sse_app(None)
9798
case "streamable-http":
9899
mcp_app = mcp_instance.streamable_http_app()
100+
case _:
101+
print("Unknown transport:", args.transport, file=sys.stderr)
102+
sys.exit(1)
103+
104+
mcp_app = CORSMiddleware(
105+
mcp_app,
106+
allow_origins=["*"],
107+
allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
108+
allow_headers=["*"],
109+
expose_headers=["Mcp-Session-Id"],
110+
)
99111

100112
uv_cfg = Config(app=mcp_app, host=args.host, port=args.port, loop="asyncio", lifespan="on", log_level="info")
101113
uv_server = Server(uv_cfg)

requirements.txt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,24 @@ mcp-haystack~=0.6.0
88
chardet~=5.2.0
99
sentence-transformers~=5.1.0
1010
prompt_toolkit~=3.0.51
11-
mcp[cli]~=1.14.1
11+
mcp[cli]~=1.17.0
1212
httpx~=0.28.1
13-
uv~=0.8.18
13+
uv~=0.9.3
1414
tldextract~=5.3.0
1515
validators~=0.35.0
1616
more-itertools~=10.8.0
1717
requests~=2.32.4
18-
beautifulsoup4~=4.13.4
19-
aiofiles~=24.1.0
18+
beautifulsoup4~=4.14.2
19+
aiofiles~=25.1.0
2020
bitarray~=3.7.0
2121
persist-queue~=1.0.0
22-
mitmproxy~=12.1.1
22+
mitmproxy~=12.2.0
2323
lxml~=6.0.0
2424
tinycss2~=1.4.0
2525
json5~=0.12.0
2626
html5lib~=1.1
2727
optimum~=1.27.0
28-
ddgs~=9.5.2
28+
ddgs~=9.6.1
2929
pycryptodome~=3.23.0
3030
psutil~=7.1.0
3131
guppy3~=3.1.5

shyhurricane/mcp_server/tools/run_unix_command.py

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import sys
44
import traceback
5+
import uuid
56
from typing import Optional, Dict, Annotated
67

78
import aiofiles
@@ -28,10 +29,12 @@ class RunCommandConfirmation(BaseModel):
2829
class RunUnixCommand(BaseModel):
2930
command: str = Field(description="The command that was run")
3031
return_code: int = Field(description="Return code of command, 0 usually means successful")
32+
output_file: Optional[str] = Field(description="Output file location containing all of standard out")
3133
output: str = Field(description="Output of command as string")
32-
output_truncated: bool = Field(description="If true, output is truncated")
34+
output_truncated: bool = Field(description="If true, output string is truncated")
35+
error_file: Optional[str] = Field(description="Error file location containing all of standard error")
3336
error: str = Field(description="Output of stderr, could be errors or progress information")
34-
error_truncated: bool = Field(description="If true, error is truncated")
37+
error_truncated: bool = Field(description="If true, error string is truncated")
3538
notes: Optional[str] = Field(description="Notes for understanding the command output or fixing failed commands")
3639

3740

@@ -85,9 +88,25 @@ async def run_unix_command(
8588
output_length_limit=output_length_limit)
8689
server_ctx = await get_server_context()
8790

91+
try:
92+
assert_elicitation(server_ctx)
93+
confirm_result = await ctx.elicit(
94+
message=f"{command}\nShould I run this command?",
95+
schema=RunCommandConfirmation)
96+
match confirm_result:
97+
case AcceptedElicitation(data=data):
98+
if not data.confirm:
99+
return None
100+
case DeclinedElicitation():
101+
return None
102+
case CancelledElicitation():
103+
return None
104+
except McpError:
105+
logger.warning("elicit not supported, continuing")
106+
88107
try:
89108
result = await _run_unix_command(ctx, command=command, additional_hosts=additional_hosts, env=env,
90-
output_length_limit=output_length_limit)
109+
output_length_limit=output_length_limit, capture_output_to_file=True)
91110

92111
if result.return_code != 0 and (
93112
"executable file not found" in result.error or "command not found" in result.error):
@@ -110,8 +129,10 @@ async def run_unix_command(
110129
return RunUnixCommand(
111130
command=command,
112131
return_code=-1,
132+
output_file=None,
113133
output="",
114134
output_truncated=False,
135+
error_file=None,
115136
error=''.join(traceback.format_exception(exc_type, exc_value, exc_tb)),
116137
error_truncated=False,
117138
notes=None,
@@ -138,8 +159,8 @@ async def _run_unix_command(
138159
stdin: Optional[str] = None,
139160
env: Optional[Dict[str, str]] = None,
140161
output_length_limit: Optional[int] = None,
141-
) -> Optional[
142-
RunUnixCommand]:
162+
capture_output_to_file: bool = False,
163+
) -> Optional[RunUnixCommand]:
143164
command = command.strip()
144165
if not command:
145166
raise McpError(ErrorData(code=INVALID_REQUEST, message="command required"))
@@ -150,30 +171,23 @@ async def _run_unix_command(
150171

151172
stdin_bytes = stdin.encode("utf-8") if stdin else None
152173

153-
try:
154-
assert_elicitation(server_ctx)
155-
confirm_result = await ctx.elicit(
156-
message=f"{command}\nShould I run this command?",
157-
schema=RunCommandConfirmation)
158-
match confirm_result:
159-
case AcceptedElicitation(data=data):
160-
if not data.confirm:
161-
return None
162-
case DeclinedElicitation():
163-
return None
164-
case CancelledElicitation():
165-
return None
166-
except McpError:
167-
logger.warning("elicit not supported, continuing")
168-
169174
output_limiter = OutputLimiter(output_length_limit)
170175
error_limiter = OutputLimiter(output_length_limit)
171176

177+
if capture_output_to_file:
178+
capture_basename = uuid.uuid4().hex
179+
capture_output_file = capture_basename + ".out"
180+
capture_error_file = capture_basename + ".err"
181+
else:
182+
capture_output_file = None
183+
capture_error_file = None
184+
172185
async with aiofiles.tempfile.TemporaryFile(mode="w+b") as stdout_file:
173186
async with aiofiles.tempfile.TemporaryFile(mode="w+b") as stderr_file:
174187
# Use a common working directory for the session to chain together commands
175188
work_path = ctx.request_context.lifespan_context.work_path
176189
docker_command = ["docker", "run", "--rm"]
190+
177191
if server_ctx.open_world:
178192
docker_command.extend([
179193
"--cap-add", "NET_BIND_SERVICE",
@@ -187,6 +201,12 @@ async def _run_unix_command(
187201
"--cap-drop", "NET_RAW",
188202
"--network", "none",
189203
])
204+
205+
if capture_output_file:
206+
docker_command.extend(["-e", f"STDOUT_LOG={capture_output_file}"])
207+
if capture_error_file:
208+
docker_command.extend(["-e", f"STDERR_LOG={capture_error_file}"])
209+
190210
docker_command.extend([
191211
"-v", f"{server_ctx.mcp_session_volume}:/work",
192212
"-v", f"{server_ctx.seclists_volume}:/usr/share/seclists",
@@ -266,8 +286,10 @@ async def _run_unix_command(
266286
return RunUnixCommand(
267287
command=command,
268288
return_code=return_code,
289+
output_file=capture_output_file,
269290
output=output,
270291
output_truncated=output_truncated,
292+
error_file=capture_error_file,
271293
error="",
272294
error_truncated=False,
273295
notes=None,
@@ -278,8 +300,10 @@ async def _run_unix_command(
278300
return RunUnixCommand(
279301
command=command,
280302
return_code=return_code,
303+
output_file=capture_output_file,
281304
output=output,
282305
output_truncated=output_truncated,
306+
error_file=capture_error_file,
283307
error=error_tail,
284308
error_truncated=error_truncated,
285309
notes=open_world_command_disable_notes if not server_ctx.open_world else None,

src/docker/unix_command/entrypoint.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,34 @@ for D in ${KEEP_DIRS}; do
1414
fi
1515
done
1616

17+
# Create FIFOs for live mirroring to tee
18+
stdout_fifo=$(mktemp -u)
19+
stderr_fifo=$(mktemp -u)
20+
mkfifo "$stdout_fifo" "$stderr_fifo"
21+
22+
# Start tee processes:
23+
# - stdout: write log AND mirror to original stdout
24+
# - stderr: write log AND mirror to original stderr
25+
tee "${STDOUT_LOG:-/dev/null}" <"$stdout_fifo" >&1 &
26+
tee_pid_out=$!
27+
tee "${STDERR_LOG:-/dev/null}" <"$stderr_fifo" >&2 &
28+
tee_pid_err=$!
29+
30+
# Redirect the script’s stdout/stderr into the FIFOs
31+
exec 1>"$stdout_fifo" 2>"$stderr_fifo"
32+
33+
# Ensure we clean up even on error/exit
34+
cleanup() {
35+
# Close current stdout/stderr to send EOF to tee
36+
exec 1>&- 2>&-
37+
# Wait for tee to flush and exit
38+
wait "$tee_pid_out" "$tee_pid_err" 2>/dev/null || true
39+
# Remove FIFOs
40+
rm -f "$stdout_fifo" "$stderr_fifo" || true
41+
}
42+
trap cleanup EXIT
43+
44+
1745
"$@"
1846
RET=$?
1947

0 commit comments

Comments
 (0)