skilllite 0.1.0__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.
@@ -0,0 +1,734 @@
1
+ """
2
+ MCP Server implementation for SkillLite.
3
+
4
+ This module provides a secure code execution sandbox server implementing
5
+ the Model Context Protocol (MCP).
6
+
7
+ Security Model:
8
+ The MCP server implements a two-phase execution model for security:
9
+
10
+ 1. **scan_code**: First, scan the code for security issues. This returns
11
+ a detailed report of any potential risks found in the code.
12
+
13
+ 2. **execute_code**: Then, execute the code. If security issues were found,
14
+ the caller must explicitly set `confirmed=true` to proceed.
15
+
16
+ This allows LLM clients to present security warnings to users and get
17
+ explicit confirmation before executing potentially dangerous code.
18
+
19
+ Environment Variables:
20
+ SKILLBOX_SANDBOX_LEVEL: Default sandbox level (1/2/3, default: 3)
21
+ SKILLBOX_PATH: Path to skillbox binary
22
+ MCP_SANDBOX_TIMEOUT: Execution timeout in seconds (default: 30)
23
+ """
24
+
25
+ import asyncio
26
+ import hashlib
27
+ import json
28
+ import os
29
+ import shutil
30
+ import subprocess
31
+ import tempfile
32
+ import time
33
+ from pathlib import Path
34
+ from typing import Any, Dict, List, Optional, Tuple
35
+
36
+
37
+ def _load_dotenv():
38
+ """Load environment variables from .env file if it exists."""
39
+ # Try to find .env file in current directory or parent directories
40
+ current_dir = Path.cwd()
41
+
42
+ for search_dir in [current_dir] + list(current_dir.parents)[:3]:
43
+ env_file = search_dir / ".env"
44
+ if env_file.exists():
45
+ try:
46
+ with open(env_file, "r") as f:
47
+ for line in f:
48
+ line = line.strip()
49
+ # Skip comments and empty lines
50
+ if not line or line.startswith("#"):
51
+ continue
52
+ # Parse KEY=VALUE
53
+ if "=" in line:
54
+ key, _, value = line.partition("=")
55
+ key = key.strip()
56
+ value = value.strip()
57
+ # Remove quotes if present
58
+ if value and value[0] in ('"', "'") and value[-1] == value[0]:
59
+ value = value[1:-1]
60
+ # Only set if not already in environment
61
+ if key and key not in os.environ:
62
+ os.environ[key] = value
63
+ return True
64
+ except Exception:
65
+ pass
66
+ return False
67
+
68
+
69
+ # Load .env file on module import
70
+ _load_dotenv()
71
+
72
+ try:
73
+ from mcp.server import Server
74
+ from mcp.server.stdio import stdio_server
75
+ from mcp.types import (
76
+ TextContent,
77
+ Tool,
78
+ CallToolResult,
79
+ )
80
+ MCP_AVAILABLE = True
81
+ except ImportError:
82
+ MCP_AVAILABLE = False
83
+ # Define stub types for when MCP is not available
84
+ Server = None # type: ignore
85
+ stdio_server = None # type: ignore
86
+ TextContent = None # type: ignore
87
+ Tool = None # type: ignore
88
+ CallToolResult = None # type: ignore
89
+
90
+
91
+ class SecurityScanResult:
92
+ """Result of a security scan."""
93
+
94
+ def __init__(
95
+ self,
96
+ is_safe: bool,
97
+ issues: List[Dict[str, Any]],
98
+ scan_id: str,
99
+ code_hash: str,
100
+ high_severity_count: int = 0,
101
+ medium_severity_count: int = 0,
102
+ low_severity_count: int = 0,
103
+ ):
104
+ self.is_safe = is_safe
105
+ self.issues = issues
106
+ self.scan_id = scan_id
107
+ self.code_hash = code_hash
108
+ self.high_severity_count = high_severity_count
109
+ self.medium_severity_count = medium_severity_count
110
+ self.low_severity_count = low_severity_count
111
+ self.timestamp = time.time()
112
+
113
+ def to_dict(self) -> Dict[str, Any]:
114
+ return {
115
+ "is_safe": self.is_safe,
116
+ "issues": self.issues,
117
+ "scan_id": self.scan_id,
118
+ "code_hash": self.code_hash,
119
+ "high_severity_count": self.high_severity_count,
120
+ "medium_severity_count": self.medium_severity_count,
121
+ "low_severity_count": self.low_severity_count,
122
+ "requires_confirmation": self.high_severity_count > 0,
123
+ }
124
+
125
+ def format_report(self) -> str:
126
+ """Format a human-readable security report."""
127
+ if not self.issues:
128
+ return "✅ Security scan passed. No issues found."
129
+
130
+ lines = [
131
+ f"📋 Security Scan Report (ID: {self.scan_id[:8]})",
132
+ f" Found {len(self.issues)} item(s) for review:",
133
+ "",
134
+ ]
135
+
136
+ severity_icons = {
137
+ "Critical": "🔴",
138
+ "High": "🟠",
139
+ "Medium": "🟡",
140
+ "Low": "🟢",
141
+ }
142
+
143
+ for idx, issue in enumerate(self.issues, 1):
144
+ severity = issue.get("severity", "Medium")
145
+ icon = severity_icons.get(severity, "⚪")
146
+ lines.append(f" {icon} #{idx} [{severity}] {issue.get('issue_type', 'Unknown')}")
147
+ lines.append(f" ├─ Rule: {issue.get('rule_id', 'N/A')}")
148
+ lines.append(f" ├─ Line {issue.get('line_number', '?')}: {issue.get('description', '')}")
149
+ lines.append(f" └─ Code: {issue.get('code_snippet', '')[:60]}...")
150
+ lines.append("")
151
+
152
+ if self.high_severity_count > 0:
153
+ lines.append("⚠️ High severity issues found. Confirmation required to execute.")
154
+ lines.append(f" To proceed, call execute_code with confirmed=true and scan_id=\"{self.scan_id}\"")
155
+ else:
156
+ lines.append("ℹ️ Only low/medium severity issues found. Safe to execute.")
157
+
158
+ return "\n".join(lines)
159
+
160
+
161
+ class SandboxExecutor:
162
+ """Secure code execution sandbox using Rust skillbox."""
163
+
164
+ # Cache scan results for confirmation flow (scan_id -> result)
165
+ _scan_cache: Dict[str, SecurityScanResult] = {}
166
+ # Cache expiry time in seconds
167
+ SCAN_CACHE_TTL = 300 # 5 minutes
168
+
169
+ def __init__(self):
170
+ from ..sandbox.skillbox import find_binary
171
+
172
+ self.skillbox_path = os.getenv("SKILLBOX_PATH") or find_binary() or "./skillbox/target/release/skillbox"
173
+ self.timeout = int(os.getenv("MCP_SANDBOX_TIMEOUT", "30"))
174
+ self.runtime_available = os.path.exists(self.skillbox_path) and os.access(self.skillbox_path, os.X_OK)
175
+
176
+ # Read default sandbox level from environment variable
177
+ # SKILLBOX_SANDBOX_LEVEL: 1=no sandbox, 2=sandbox only, 3=sandbox+scan (default)
178
+ default_level = os.getenv("SKILLBOX_SANDBOX_LEVEL", "3")
179
+ try:
180
+ self.default_sandbox_level = int(default_level)
181
+ if self.default_sandbox_level not in [1, 2, 3]:
182
+ self.default_sandbox_level = 3
183
+ except ValueError:
184
+ self.default_sandbox_level = 3
185
+
186
+ def _generate_code_hash(self, language: str, code: str) -> str:
187
+ """Generate a hash of the code for verification."""
188
+ content = f"{language}:{code}"
189
+ return hashlib.sha256(content.encode()).hexdigest()
190
+
191
+ def _generate_scan_id(self, code_hash: str) -> str:
192
+ """Generate a unique scan ID."""
193
+ timestamp = str(time.time())
194
+ return hashlib.sha256(f"{code_hash}:{timestamp}".encode()).hexdigest()[:16]
195
+
196
+ def _cleanup_expired_scans(self):
197
+ """Remove expired scan results from cache."""
198
+ current_time = time.time()
199
+ expired_ids = [
200
+ scan_id for scan_id, result in self._scan_cache.items()
201
+ if current_time - result.timestamp > self.SCAN_CACHE_TTL
202
+ ]
203
+ for scan_id in expired_ids:
204
+ del self._scan_cache[scan_id]
205
+
206
+ def _create_temp_skill(self, language: str, code: str) -> Tuple[str, str]:
207
+ """Create a temporary skill directory with the code file."""
208
+ skill_dir = tempfile.mkdtemp(prefix="mcp_skill_")
209
+
210
+ # Create scripts subdirectory (required by skillbox)
211
+ scripts_dir = os.path.join(skill_dir, "scripts")
212
+ os.makedirs(scripts_dir, exist_ok=True)
213
+
214
+ ext = self.get_file_extension(language)
215
+ entry_point = f"scripts/main.{ext}"
216
+
217
+ skill_md_content = f"""---
218
+ name: mcp-execution
219
+ entry_point: {entry_point}
220
+ language: {language}
221
+ description: MCP code execution skill
222
+ network:
223
+ enabled: true
224
+ ---
225
+
226
+ This skill executes code from MCP.
227
+ """
228
+ with open(os.path.join(skill_dir, "SKILL.md"), "w") as f:
229
+ f.write(skill_md_content)
230
+
231
+ code_file = os.path.join(scripts_dir, f"main.{ext}")
232
+ with open(code_file, "w") as f:
233
+ f.write(code)
234
+ os.chmod(code_file, 0o755)
235
+
236
+ return skill_dir, code_file
237
+
238
+ def scan_code(self, language: str, code: str) -> SecurityScanResult:
239
+ """Scan code for security issues without executing it."""
240
+ if not self.runtime_available:
241
+ return SecurityScanResult(
242
+ is_safe=False,
243
+ issues=[{"severity": "Critical", "issue_type": "RuntimeError",
244
+ "description": f"skillbox not found at {self.skillbox_path}",
245
+ "rule_id": "system", "line_number": 0, "code_snippet": ""}],
246
+ scan_id="error",
247
+ code_hash="",
248
+ high_severity_count=1,
249
+ )
250
+
251
+ self._cleanup_expired_scans()
252
+ code_hash = self._generate_code_hash(language, code)
253
+ scan_id = self._generate_scan_id(code_hash)
254
+
255
+ try:
256
+ skill_dir, code_file = self._create_temp_skill(language, code)
257
+
258
+ try:
259
+ result = subprocess.run(
260
+ [self.skillbox_path, "security-scan", code_file],
261
+ capture_output=True,
262
+ text=True,
263
+ timeout=30
264
+ )
265
+
266
+ issues = self._parse_scan_output(result.stdout + result.stderr)
267
+ high_count = sum(1 for i in issues if i.get("severity") in ["Critical", "High"])
268
+ medium_count = sum(1 for i in issues if i.get("severity") == "Medium")
269
+ low_count = sum(1 for i in issues if i.get("severity") == "Low")
270
+
271
+ scan_result = SecurityScanResult(
272
+ is_safe=high_count == 0,
273
+ issues=issues,
274
+ scan_id=scan_id,
275
+ code_hash=code_hash,
276
+ high_severity_count=high_count,
277
+ medium_severity_count=medium_count,
278
+ low_severity_count=low_count,
279
+ )
280
+
281
+ self._scan_cache[scan_id] = scan_result
282
+ return scan_result
283
+
284
+ finally:
285
+ shutil.rmtree(skill_dir, ignore_errors=True)
286
+
287
+ except subprocess.TimeoutExpired:
288
+ return SecurityScanResult(
289
+ is_safe=False,
290
+ issues=[{"severity": "Critical", "issue_type": "Timeout",
291
+ "description": "Security scan timed out",
292
+ "rule_id": "system", "line_number": 0, "code_snippet": ""}],
293
+ scan_id=scan_id,
294
+ code_hash=code_hash,
295
+ high_severity_count=1,
296
+ )
297
+ except Exception as e:
298
+ return SecurityScanResult(
299
+ is_safe=False,
300
+ issues=[{"severity": "Critical", "issue_type": "ScanError",
301
+ "description": str(e),
302
+ "rule_id": "system", "line_number": 0, "code_snippet": ""}],
303
+ scan_id=scan_id,
304
+ code_hash=code_hash,
305
+ high_severity_count=1,
306
+ )
307
+
308
+ def _parse_scan_output(self, output: str) -> List[Dict[str, Any]]:
309
+ """Parse security scan output into structured issues."""
310
+ issues = []
311
+ lines = output.split("\n")
312
+
313
+ current_issue = None
314
+ for line in lines:
315
+ line = line.strip()
316
+
317
+ if line.startswith(("🔴", "🟠", "🟡", "🟢", "🚨")):
318
+ if current_issue:
319
+ issues.append(current_issue)
320
+
321
+ severity = "Medium"
322
+ if "🔴" in line or "🚨" in line:
323
+ severity = "Critical" if "🚨" in line else "High"
324
+ elif "🟠" in line:
325
+ severity = "High"
326
+ elif "🟡" in line:
327
+ severity = "Medium"
328
+ elif "🟢" in line:
329
+ severity = "Low"
330
+
331
+ issue_type = "Unknown"
332
+ if "[" in line and "]" in line:
333
+ start = line.index("[") + 1
334
+ end = line.index("]")
335
+ issue_type = line[end+1:].strip() if end + 1 < len(line) else "Unknown"
336
+
337
+ current_issue = {
338
+ "severity": severity,
339
+ "issue_type": issue_type,
340
+ "rule_id": "",
341
+ "line_number": 0,
342
+ "description": "",
343
+ "code_snippet": "",
344
+ }
345
+ elif current_issue:
346
+ if "Rule:" in line:
347
+ current_issue["rule_id"] = line.split("Rule:")[-1].strip()
348
+ elif "Line" in line and ":" in line:
349
+ parts = line.split(":", 1)
350
+ if len(parts) > 1:
351
+ try:
352
+ line_part = parts[0].replace("├─", "").replace("Line", "").strip()
353
+ current_issue["line_number"] = int(line_part)
354
+ current_issue["description"] = parts[1].strip()
355
+ except ValueError:
356
+ current_issue["description"] = line
357
+ elif "Code:" in line:
358
+ current_issue["code_snippet"] = line.split("Code:")[-1].strip()
359
+
360
+ if current_issue:
361
+ issues.append(current_issue)
362
+
363
+ return issues
364
+
365
+ def verify_scan(self, scan_id: str, code_hash: str) -> Optional[SecurityScanResult]:
366
+ """Verify a scan result exists and matches the code hash."""
367
+ self._cleanup_expired_scans()
368
+
369
+ if scan_id not in self._scan_cache:
370
+ return None
371
+
372
+ result = self._scan_cache[scan_id]
373
+ if result.code_hash != code_hash:
374
+ return None
375
+
376
+ return result
377
+
378
+ def execute(
379
+ self,
380
+ language: str,
381
+ code: str,
382
+ confirmed: bool = False,
383
+ scan_id: Optional[str] = None,
384
+ sandbox_level: Optional[int] = None,
385
+ ) -> Dict[str, Any]:
386
+ """Execute code in a secure sandbox using Rust skillbox.
387
+
388
+ Args:
389
+ language: Programming language (python, javascript, bash)
390
+ code: Code to execute
391
+ confirmed: Whether user has confirmed execution despite security warnings
392
+ scan_id: Scan ID from previous scan_code call (required when confirmed=True)
393
+ sandbox_level: Override sandbox level (default: from SKILLBOX_SANDBOX_LEVEL env or 3)
394
+ """
395
+ # Use default sandbox level from environment if not specified
396
+ if sandbox_level is None:
397
+ sandbox_level = self.default_sandbox_level
398
+ if not self.runtime_available:
399
+ return {
400
+ "success": False,
401
+ "stdout": "",
402
+ "stderr": f"skillbox not found at {self.skillbox_path}. Please build it with: cd skillbox && cargo build --release",
403
+ "exit_code": 1
404
+ }
405
+
406
+ code_hash = self._generate_code_hash(language, code)
407
+
408
+ if sandbox_level >= 3 and not confirmed:
409
+ scan_result = self.scan_code(language, code)
410
+ if scan_result.high_severity_count > 0:
411
+ return {
412
+ "success": False,
413
+ "stdout": "",
414
+ "stderr": (
415
+ f"🔐 Security Review Required\n\n"
416
+ f"{scan_result.format_report()}\n\n"
417
+ f"To execute this code, call execute_code again with:\n"
418
+ f" - confirmed: true\n"
419
+ f" - scan_id: \"{scan_result.scan_id}\"\n"
420
+ ),
421
+ "exit_code": 2,
422
+ "requires_confirmation": True,
423
+ "scan_id": scan_result.scan_id,
424
+ "security_issues": scan_result.to_dict(),
425
+ }
426
+
427
+ if confirmed and scan_id:
428
+ cached_result = self.verify_scan(scan_id, code_hash)
429
+ if not cached_result:
430
+ return {
431
+ "success": False,
432
+ "stdout": "",
433
+ "stderr": (
434
+ "❌ Invalid or expired scan_id. The code may have been modified.\n"
435
+ "Please run scan_code again to get a new scan_id."
436
+ ),
437
+ "exit_code": 3,
438
+ }
439
+
440
+ try:
441
+ skill_dir, _ = self._create_temp_skill(language, code)
442
+
443
+ try:
444
+ env = os.environ.copy()
445
+ env["SKILLBOX_AUTO_APPROVE"] = "true"
446
+
447
+ cmd = [self.skillbox_path, "run"]
448
+ if sandbox_level in [1, 2, 3]:
449
+ cmd.extend(["--sandbox-level", str(sandbox_level)])
450
+ cmd.extend([skill_dir, "{}"])
451
+
452
+ result = subprocess.run(
453
+ cmd,
454
+ capture_output=True,
455
+ text=True,
456
+ timeout=self.timeout,
457
+ env=env,
458
+ )
459
+
460
+ return {
461
+ "success": result.returncode == 0,
462
+ "stdout": result.stdout,
463
+ "stderr": result.stderr,
464
+ "exit_code": result.returncode
465
+ }
466
+ finally:
467
+ shutil.rmtree(skill_dir, ignore_errors=True)
468
+
469
+ except subprocess.TimeoutExpired:
470
+ return {
471
+ "success": False,
472
+ "stdout": "",
473
+ "stderr": f"Execution timed out after {self.timeout} seconds",
474
+ "exit_code": 124
475
+ }
476
+ except Exception as e:
477
+ return {
478
+ "success": False,
479
+ "stdout": "",
480
+ "stderr": str(e),
481
+ "exit_code": 1
482
+ }
483
+
484
+ def get_file_extension(self, language: str) -> str:
485
+ """Get file extension for the given language."""
486
+ extensions = {
487
+ "python": "py",
488
+ "javascript": "js",
489
+ "bash": "sh"
490
+ }
491
+ return extensions.get(language, "py")
492
+
493
+
494
+ class MCPServer:
495
+ """MCP server for SkillLite sandbox execution.
496
+
497
+ This server provides two tools for secure code execution:
498
+
499
+ 1. **scan_code**: Scan code for security issues before execution.
500
+ Returns a detailed report and a scan_id for confirmation.
501
+
502
+ 2. **execute_code**: Execute code in a sandbox. If high-severity
503
+ security issues are found, requires explicit confirmation.
504
+
505
+ Example workflow:
506
+ 1. Call scan_code to check for security issues
507
+ 2. Review the security report with the user
508
+ 3. If user approves, call execute_code with confirmed=true and scan_id
509
+ """
510
+
511
+ def __init__(self):
512
+ if not MCP_AVAILABLE:
513
+ raise ImportError(
514
+ "MCP library not available. Please install it with: "
515
+ "pip install skilllite[mcp]"
516
+ )
517
+ self.server = Server("skilllite-mcp-server")
518
+ self.executor = SandboxExecutor()
519
+ self._setup_handlers()
520
+
521
+ def _setup_handlers(self):
522
+ """Setup MCP server handlers."""
523
+
524
+ @self.server.list_tools()
525
+ async def list_tools() -> List[Tool]:
526
+ return [
527
+ Tool(
528
+ name="scan_code",
529
+ description=(
530
+ "Scan code for security issues before execution. "
531
+ "Returns a security report with any potential risks found. "
532
+ "Use this before execute_code to review security implications."
533
+ ),
534
+ inputSchema={
535
+ "type": "object",
536
+ "properties": {
537
+ "language": {
538
+ "type": "string",
539
+ "enum": ["python", "javascript", "bash"],
540
+ "description": "Programming language of the code"
541
+ },
542
+ "code": {
543
+ "type": "string",
544
+ "description": "Code to scan for security issues"
545
+ }
546
+ },
547
+ "required": ["language", "code"]
548
+ }
549
+ ),
550
+ Tool(
551
+ name="execute_code",
552
+ description=(
553
+ "Execute code in a secure sandbox environment. "
554
+ "If security issues are found, you must set confirmed=true "
555
+ "and provide the scan_id from a previous scan_code call."
556
+ ),
557
+ inputSchema={
558
+ "type": "object",
559
+ "properties": {
560
+ "language": {
561
+ "type": "string",
562
+ "enum": ["python", "javascript", "bash"],
563
+ "description": "Programming language to execute"
564
+ },
565
+ "code": {
566
+ "type": "string",
567
+ "description": "Code to execute"
568
+ },
569
+ "confirmed": {
570
+ "type": "boolean",
571
+ "description": (
572
+ "Set to true to confirm execution despite security warnings. "
573
+ "Required when high-severity issues are found."
574
+ ),
575
+ "default": False
576
+ },
577
+ "scan_id": {
578
+ "type": "string",
579
+ "description": (
580
+ "The scan_id from a previous scan_code call. "
581
+ "Required when confirmed=true to verify the code hasn't changed."
582
+ )
583
+ },
584
+ "sandbox_level": {
585
+ "type": "integer",
586
+ "enum": [1, 2, 3],
587
+ "description": (
588
+ "Sandbox security level: "
589
+ "1=no sandbox, 2=sandbox only, 3=sandbox+security scan (default)"
590
+ ),
591
+ "default": 3
592
+ }
593
+ },
594
+ "required": ["language", "code"]
595
+ }
596
+ )
597
+ ]
598
+
599
+ @self.server.call_tool()
600
+ async def call_tool(
601
+ name: str,
602
+ arguments: Dict[str, Any]
603
+ ) -> "CallToolResult":
604
+ if name == "scan_code":
605
+ return await self._handle_scan_code(arguments)
606
+ elif name == "execute_code":
607
+ return await self._handle_execute_code(arguments)
608
+ else:
609
+ return CallToolResult(
610
+ isError=True,
611
+ content=[
612
+ TextContent(
613
+ type="text",
614
+ text=f"Unknown tool: {name}"
615
+ )
616
+ ]
617
+ )
618
+
619
+ async def _handle_scan_code(self, arguments: Dict[str, Any]) -> "CallToolResult":
620
+ """Handle scan_code tool call."""
621
+ language = arguments.get("language")
622
+ code = arguments.get("code")
623
+
624
+ if not language or not code:
625
+ return CallToolResult(
626
+ isError=True,
627
+ content=[
628
+ TextContent(
629
+ type="text",
630
+ text="Missing required arguments: language and code"
631
+ )
632
+ ]
633
+ )
634
+
635
+ scan_result = self.executor.scan_code(language, code)
636
+
637
+ report = scan_result.format_report()
638
+
639
+ result_json = json.dumps(scan_result.to_dict(), indent=2)
640
+
641
+ return CallToolResult(
642
+ content=[
643
+ TextContent(
644
+ type="text",
645
+ text=f"{report}\n\n---\nScan Details (JSON):\n{result_json}"
646
+ )
647
+ ]
648
+ )
649
+
650
+ async def _handle_execute_code(self, arguments: Dict[str, Any]) -> "CallToolResult":
651
+ """Handle execute_code tool call."""
652
+ language = arguments.get("language")
653
+ code = arguments.get("code")
654
+ confirmed = arguments.get("confirmed", False)
655
+ scan_id = arguments.get("scan_id")
656
+ # sandbox_level: None means use default from environment variable
657
+ sandbox_level = arguments.get("sandbox_level")
658
+
659
+ if not language or not code:
660
+ return CallToolResult(
661
+ isError=True,
662
+ content=[
663
+ TextContent(
664
+ type="text",
665
+ text="Missing required arguments: language and code"
666
+ )
667
+ ]
668
+ )
669
+
670
+ result = self.executor.execute(
671
+ language=language,
672
+ code=code,
673
+ confirmed=confirmed,
674
+ scan_id=scan_id,
675
+ sandbox_level=sandbox_level,
676
+ )
677
+
678
+ if result.get("requires_confirmation"):
679
+ return CallToolResult(
680
+ content=[
681
+ TextContent(
682
+ type="text",
683
+ text=result["stderr"]
684
+ )
685
+ ]
686
+ )
687
+
688
+ output_lines = []
689
+ if result["stdout"]:
690
+ output_lines.append(f"Output:\n{result['stdout']}")
691
+ if result["stderr"]:
692
+ output_lines.append(f"Errors:\n{result['stderr']}")
693
+
694
+ output_text = "\n".join(output_lines) if output_lines else "Execution completed successfully (no output)"
695
+
696
+ if result["success"]:
697
+ return CallToolResult(
698
+ content=[
699
+ TextContent(
700
+ type="text",
701
+ text=output_text
702
+ )
703
+ ]
704
+ )
705
+ else:
706
+ return CallToolResult(
707
+ isError=True,
708
+ content=[
709
+ TextContent(
710
+ type="text",
711
+ text=f"Execution failed:\n{output_text}"
712
+ )
713
+ ]
714
+ )
715
+
716
+ async def run(self):
717
+ """Run the MCP server."""
718
+ async with stdio_server() as (read_stream, write_stream):
719
+ await self.server.run(read_stream, write_stream, self.server.create_initialization_options())
720
+
721
+
722
+ async def main():
723
+ """Main entry point for the MCP sandbox server."""
724
+ if not MCP_AVAILABLE:
725
+ print("Error: MCP library not available")
726
+ print("Please install it with: pip install skilllite[mcp]")
727
+ return
728
+
729
+ server = MCPServer()
730
+ await server.run()
731
+
732
+
733
+ if __name__ == "__main__":
734
+ asyncio.run(main())