skilllite 0.1.0__py3-none-any.whl → 0.1.2__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.
skilllite/mcp/server.py CHANGED
@@ -4,15 +4,22 @@ MCP Server implementation for SkillLite.
4
4
  This module provides a secure code execution sandbox server implementing
5
5
  the Model Context Protocol (MCP).
6
6
 
7
+ Features:
8
+ 1. **Skills Management**: List, inspect, and execute pre-defined skills
9
+ from the skills directory.
10
+
11
+ 2. **Code Execution**: Execute arbitrary code in a secure sandbox with
12
+ security scanning.
13
+
7
14
  Security Model:
8
15
  The MCP server implements a two-phase execution model for security:
9
-
16
+
10
17
  1. **scan_code**: First, scan the code for security issues. This returns
11
18
  a detailed report of any potential risks found in the code.
12
-
19
+
13
20
  2. **execute_code**: Then, execute the code. If security issues were found,
14
21
  the caller must explicitly set `confirmed=true` to proceed.
15
-
22
+
16
23
  This allows LLM clients to present security warnings to users and get
17
24
  explicit confirmation before executing potentially dangerous code.
18
25
 
@@ -20,6 +27,7 @@ Environment Variables:
20
27
  SKILLBOX_SANDBOX_LEVEL: Default sandbox level (1/2/3, default: 3)
21
28
  SKILLBOX_PATH: Path to skillbox binary
22
29
  MCP_SANDBOX_TIMEOUT: Execution timeout in seconds (default: 30)
30
+ SKILLLITE_SKILLS_DIR: Directory containing skills (default: ./.skills)
23
31
  """
24
32
 
25
33
  import asyncio
@@ -90,7 +98,27 @@ except ImportError:
90
98
 
91
99
  class SecurityScanResult:
92
100
  """Result of a security scan."""
93
-
101
+
102
+ # Issue types that are HARD BLOCKED in L3 sandbox (cannot execute even with confirmation)
103
+ # These operations are blocked at the sandbox runtime level, not just static analysis
104
+ HARD_BLOCKED_ISSUE_TYPES_L3 = {
105
+ "Process Execution", # os.system, subprocess, etc.
106
+ "ProcessExecution", # Alternative format
107
+ "process_execution", # Snake case format
108
+ }
109
+
110
+ # Rule IDs that are specifically hard blocked in L3 sandbox
111
+ HARD_BLOCKED_RULE_IDS_L3 = {
112
+ "py-subprocess", # subprocess.call/run/Popen
113
+ "py-os-system", # os.system/popen/spawn
114
+ "js-child-process", # child_process.exec/spawn
115
+ }
116
+
117
+ # Dangerous module imports that lead to hard blocks when combined with execution
118
+ HARD_BLOCKED_MODULES_L3 = {
119
+ "py-os-import", # import os/subprocess/shutil
120
+ }
121
+
94
122
  def __init__(
95
123
  self,
96
124
  is_safe: bool,
@@ -100,6 +128,7 @@ class SecurityScanResult:
100
128
  high_severity_count: int = 0,
101
129
  medium_severity_count: int = 0,
102
130
  low_severity_count: int = 0,
131
+ sandbox_level: int = 3,
103
132
  ):
104
133
  self.is_safe = is_safe
105
134
  self.issues = issues
@@ -108,8 +137,31 @@ class SecurityScanResult:
108
137
  self.high_severity_count = high_severity_count
109
138
  self.medium_severity_count = medium_severity_count
110
139
  self.low_severity_count = low_severity_count
140
+ self.sandbox_level = sandbox_level
111
141
  self.timestamp = time.time()
112
-
142
+
143
+ # Calculate hard blocked issues
144
+ self.hard_blocked_issues = self._find_hard_blocked_issues()
145
+ self.has_hard_blocked = len(self.hard_blocked_issues) > 0
146
+
147
+ def _find_hard_blocked_issues(self) -> List[Dict[str, Any]]:
148
+ """Find issues that are hard blocked in the current sandbox level."""
149
+ if self.sandbox_level < 3:
150
+ # Only L3 has hard blocks
151
+ return []
152
+
153
+ hard_blocked = []
154
+ for issue in self.issues:
155
+ issue_type = issue.get("issue_type", "")
156
+ rule_id = issue.get("rule_id", "")
157
+
158
+ # Check if this issue type or rule is hard blocked
159
+ if (issue_type in self.HARD_BLOCKED_ISSUE_TYPES_L3 or
160
+ rule_id in self.HARD_BLOCKED_RULE_IDS_L3):
161
+ hard_blocked.append(issue)
162
+
163
+ return hard_blocked
164
+
113
165
  def to_dict(self) -> Dict[str, Any]:
114
166
  return {
115
167
  "is_safe": self.is_safe,
@@ -119,42 +171,64 @@ class SecurityScanResult:
119
171
  "high_severity_count": self.high_severity_count,
120
172
  "medium_severity_count": self.medium_severity_count,
121
173
  "low_severity_count": self.low_severity_count,
122
- "requires_confirmation": self.high_severity_count > 0,
174
+ "requires_confirmation": self.high_severity_count > 0 and not self.has_hard_blocked,
175
+ "has_hard_blocked": self.has_hard_blocked,
176
+ "hard_blocked_count": len(self.hard_blocked_issues),
177
+ "sandbox_level": self.sandbox_level,
123
178
  }
124
-
179
+
125
180
  def format_report(self) -> str:
126
181
  """Format a human-readable security report."""
127
182
  if not self.issues:
128
183
  return "✅ Security scan passed. No issues found."
129
-
184
+
130
185
  lines = [
131
186
  f"📋 Security Scan Report (ID: {self.scan_id[:8]})",
187
+ f" Sandbox Level: L{self.sandbox_level}",
132
188
  f" Found {len(self.issues)} item(s) for review:",
133
189
  "",
134
190
  ]
135
-
191
+
136
192
  severity_icons = {
137
193
  "Critical": "🔴",
138
194
  "High": "🟠",
139
195
  "Medium": "🟡",
140
196
  "Low": "🟢",
141
197
  }
142
-
198
+
143
199
  for idx, issue in enumerate(self.issues, 1):
144
200
  severity = issue.get("severity", "Medium")
145
201
  icon = severity_icons.get(severity, "⚪")
146
- lines.append(f" {icon} #{idx} [{severity}] {issue.get('issue_type', 'Unknown')}")
202
+
203
+ # Mark hard blocked issues
204
+ is_hard_blocked = issue in self.hard_blocked_issues
205
+ block_marker = " 🚫 [HARD BLOCKED]" if is_hard_blocked else ""
206
+
207
+ lines.append(f" {icon} #{idx} [{severity}] {issue.get('issue_type', 'Unknown')}{block_marker}")
147
208
  lines.append(f" ├─ Rule: {issue.get('rule_id', 'N/A')}")
148
209
  lines.append(f" ├─ Line {issue.get('line_number', '?')}: {issue.get('description', '')}")
149
210
  lines.append(f" └─ Code: {issue.get('code_snippet', '')[:60]}...")
150
211
  lines.append("")
151
-
152
- if self.high_severity_count > 0:
212
+
213
+ # Different messages based on whether there are hard blocked issues
214
+ if self.has_hard_blocked:
215
+ lines.append("🚫 HARD BLOCKED: This code contains operations that CANNOT be executed")
216
+ lines.append(f" in the current L{self.sandbox_level} sandbox environment.")
217
+ lines.append("")
218
+ lines.append(" The following operations are permanently blocked at runtime:")
219
+ for issue in self.hard_blocked_issues:
220
+ lines.append(f" • {issue.get('issue_type', 'Unknown')}: {issue.get('description', '')}")
221
+ lines.append("")
222
+ lines.append(" ⚠️ Even with confirmation, this code will fail to execute.")
223
+ lines.append(" Options:")
224
+ lines.append(" 1. Modify the code to remove blocked operations")
225
+ lines.append(" 2. Use a lower sandbox level (L1 or L2) if permitted")
226
+ elif self.high_severity_count > 0:
153
227
  lines.append("⚠️ High severity issues found. Confirmation required to execute.")
154
228
  lines.append(f" To proceed, call execute_code with confirmed=true and scan_id=\"{self.scan_id}\"")
155
229
  else:
156
230
  lines.append("ℹ️ Only low/medium severity issues found. Safe to execute.")
157
-
231
+
158
232
  return "\n".join(lines)
159
233
 
160
234
 
@@ -235,26 +309,37 @@ This skill executes code from MCP.
235
309
 
236
310
  return skill_dir, code_file
237
311
 
238
- def scan_code(self, language: str, code: str) -> SecurityScanResult:
239
- """Scan code for security issues without executing it."""
312
+ def scan_code(self, language: str, code: str, sandbox_level: Optional[int] = None) -> SecurityScanResult:
313
+ """Scan code for security issues without executing it.
314
+
315
+ Args:
316
+ language: Programming language (python, javascript, bash)
317
+ code: Code to scan
318
+ sandbox_level: Sandbox level to check against (default: from env or 3)
319
+ """
320
+ # Use default sandbox level if not specified
321
+ if sandbox_level is None:
322
+ sandbox_level = self.default_sandbox_level
323
+
240
324
  if not self.runtime_available:
241
325
  return SecurityScanResult(
242
326
  is_safe=False,
243
- issues=[{"severity": "Critical", "issue_type": "RuntimeError",
327
+ issues=[{"severity": "Critical", "issue_type": "RuntimeError",
244
328
  "description": f"skillbox not found at {self.skillbox_path}",
245
329
  "rule_id": "system", "line_number": 0, "code_snippet": ""}],
246
330
  scan_id="error",
247
331
  code_hash="",
248
332
  high_severity_count=1,
333
+ sandbox_level=sandbox_level,
249
334
  )
250
-
335
+
251
336
  self._cleanup_expired_scans()
252
337
  code_hash = self._generate_code_hash(language, code)
253
338
  scan_id = self._generate_scan_id(code_hash)
254
-
339
+
255
340
  try:
256
341
  skill_dir, code_file = self._create_temp_skill(language, code)
257
-
342
+
258
343
  try:
259
344
  result = subprocess.run(
260
345
  [self.skillbox_path, "security-scan", code_file],
@@ -262,12 +347,12 @@ This skill executes code from MCP.
262
347
  text=True,
263
348
  timeout=30
264
349
  )
265
-
350
+
266
351
  issues = self._parse_scan_output(result.stdout + result.stderr)
267
352
  high_count = sum(1 for i in issues if i.get("severity") in ["Critical", "High"])
268
353
  medium_count = sum(1 for i in issues if i.get("severity") == "Medium")
269
354
  low_count = sum(1 for i in issues if i.get("severity") == "Low")
270
-
355
+
271
356
  scan_result = SecurityScanResult(
272
357
  is_safe=high_count == 0,
273
358
  issues=issues,
@@ -276,14 +361,15 @@ This skill executes code from MCP.
276
361
  high_severity_count=high_count,
277
362
  medium_severity_count=medium_count,
278
363
  low_severity_count=low_count,
364
+ sandbox_level=sandbox_level,
279
365
  )
280
-
366
+
281
367
  self._scan_cache[scan_id] = scan_result
282
368
  return scan_result
283
-
369
+
284
370
  finally:
285
371
  shutil.rmtree(skill_dir, ignore_errors=True)
286
-
372
+
287
373
  except subprocess.TimeoutExpired:
288
374
  return SecurityScanResult(
289
375
  is_safe=False,
@@ -293,6 +379,7 @@ This skill executes code from MCP.
293
379
  scan_id=scan_id,
294
380
  code_hash=code_hash,
295
381
  high_severity_count=1,
382
+ sandbox_level=sandbox_level,
296
383
  )
297
384
  except Exception as e:
298
385
  return SecurityScanResult(
@@ -303,6 +390,7 @@ This skill executes code from MCP.
303
390
  scan_id=scan_id,
304
391
  code_hash=code_hash,
305
392
  high_severity_count=1,
393
+ sandbox_level=sandbox_level,
306
394
  )
307
395
 
308
396
  def _parse_scan_output(self, output: str) -> List[Dict[str, Any]]:
@@ -404,9 +492,31 @@ This skill executes code from MCP.
404
492
  }
405
493
 
406
494
  code_hash = self._generate_code_hash(language, code)
407
-
495
+
408
496
  if sandbox_level >= 3 and not confirmed:
409
- scan_result = self.scan_code(language, code)
497
+ scan_result = self.scan_code(language, code, sandbox_level=sandbox_level)
498
+
499
+ # Check for hard blocked issues first
500
+ if scan_result.has_hard_blocked:
501
+ return {
502
+ "success": False,
503
+ "stdout": "",
504
+ "stderr": (
505
+ f"🚫 Execution Blocked\n\n"
506
+ f"{scan_result.format_report()}\n\n"
507
+ f"❌ This code contains operations that are PERMANENTLY BLOCKED\n"
508
+ f" in the L{sandbox_level} sandbox environment.\n\n"
509
+ f" Even with confirmation, this code CANNOT be executed.\n\n"
510
+ f"Options:\n"
511
+ f" 1. Modify the code to remove blocked operations\n"
512
+ f" 2. Use sandbox_level=1 or sandbox_level=2 (if permitted)\n"
513
+ ),
514
+ "exit_code": 4,
515
+ "hard_blocked": True,
516
+ "security_issues": scan_result.to_dict(),
517
+ }
518
+
519
+ # Soft risk: can be confirmed
410
520
  if scan_result.high_severity_count > 0:
411
521
  return {
412
522
  "success": False,
@@ -414,7 +524,9 @@ This skill executes code from MCP.
414
524
  "stderr": (
415
525
  f"🔐 Security Review Required\n\n"
416
526
  f"{scan_result.format_report()}\n\n"
417
- f"To execute this code, call execute_code again with:\n"
527
+ f"⚠️ IMPORTANT: You MUST ask the user for confirmation before proceeding.\n"
528
+ f"Show this security report to the user and wait for their explicit approval.\n\n"
529
+ f"If the user approves, call execute_code again with:\n"
418
530
  f" - confirmed: true\n"
419
531
  f" - scan_id: \"{scan_result.scan_id}\"\n"
420
532
  ),
@@ -423,7 +535,7 @@ This skill executes code from MCP.
423
535
  "scan_id": scan_result.scan_id,
424
536
  "security_issues": scan_result.to_dict(),
425
537
  }
426
-
538
+
427
539
  if confirmed and scan_id:
428
540
  cached_result = self.verify_scan(scan_id, code_hash)
429
541
  if not cached_result:
@@ -436,6 +548,28 @@ This skill executes code from MCP.
436
548
  ),
437
549
  "exit_code": 3,
438
550
  }
551
+
552
+ # Even with confirmation, check for hard blocked issues
553
+ if cached_result.has_hard_blocked:
554
+ return {
555
+ "success": False,
556
+ "stdout": "",
557
+ "stderr": (
558
+ f"🚫 Execution Blocked (Even After Confirmation)\n\n"
559
+ f"The code contains operations that are PERMANENTLY BLOCKED\n"
560
+ f"in the L{sandbox_level} sandbox environment:\n\n"
561
+ + "\n".join(f" • {issue.get('issue_type', 'Unknown')}: {issue.get('description', '')}"
562
+ for issue in cached_result.hard_blocked_issues) +
563
+ f"\n\n"
564
+ f"❌ Confirmation cannot override sandbox runtime restrictions.\n\n"
565
+ f"Options:\n"
566
+ f" 1. Modify the code to remove blocked operations\n"
567
+ f" 2. Use sandbox_level=1 or sandbox_level=2 (if permitted)\n"
568
+ ),
569
+ "exit_code": 4,
570
+ "hard_blocked": True,
571
+ "security_issues": cached_result.to_dict(),
572
+ }
439
573
 
440
574
  try:
441
575
  skill_dir, _ = self._create_temp_skill(language, code)
@@ -494,21 +628,31 @@ This skill executes code from MCP.
494
628
  class MCPServer:
495
629
  """MCP server for SkillLite sandbox execution.
496
630
 
497
- This server provides two tools for secure code execution:
498
-
499
- 1. **scan_code**: Scan code for security issues before execution.
631
+ This server provides tools for skills management and secure code execution:
632
+
633
+ Skills Tools:
634
+ 1. **list_skills**: List all available skills in the skills directory.
635
+ 2. **get_skill_info**: Get detailed information about a specific skill.
636
+ 3. **run_skill**: Execute a skill with given input parameters.
637
+
638
+ Code Execution Tools:
639
+ 4. **scan_code**: Scan code for security issues before execution.
500
640
  Returns a detailed report and a scan_id for confirmation.
501
-
502
- 2. **execute_code**: Execute code in a sandbox. If high-severity
641
+ 5. **execute_code**: Execute code in a sandbox. If high-severity
503
642
  security issues are found, requires explicit confirmation.
504
-
505
- Example workflow:
643
+
644
+ Example workflow for skills:
645
+ 1. Call list_skills to see available skills
646
+ 2. Call get_skill_info to understand a skill's parameters
647
+ 3. Call run_skill with the required input
648
+
649
+ Example workflow for code execution:
506
650
  1. Call scan_code to check for security issues
507
651
  2. Review the security report with the user
508
652
  3. If user approves, call execute_code with confirmed=true and scan_id
509
653
  """
510
-
511
- def __init__(self):
654
+
655
+ def __init__(self, skills_dir: Optional[str] = None):
512
656
  if not MCP_AVAILABLE:
513
657
  raise ImportError(
514
658
  "MCP library not available. Please install it with: "
@@ -516,14 +660,98 @@ class MCPServer:
516
660
  )
517
661
  self.server = Server("skilllite-mcp-server")
518
662
  self.executor = SandboxExecutor()
663
+
664
+ # Initialize SkillManager for skills support
665
+ self.skills_dir = skills_dir or os.environ.get("SKILLLITE_SKILLS_DIR", "./.skills")
666
+ self.skill_manager = None
667
+ self._init_skill_manager()
668
+
519
669
  self._setup_handlers()
670
+
671
+ def _init_skill_manager(self):
672
+ """Initialize the SkillManager if skills directory exists."""
673
+ try:
674
+ from ..core.manager import SkillManager
675
+ skills_path = Path(self.skills_dir)
676
+ if skills_path.exists():
677
+ self.skill_manager = SkillManager(skills_dir=str(skills_path))
678
+ except Exception as e:
679
+ # Skills not available, continue without them
680
+ pass
520
681
 
521
682
  def _setup_handlers(self):
522
683
  """Setup MCP server handlers."""
523
-
684
+
524
685
  @self.server.list_tools()
525
686
  async def list_tools() -> List[Tool]:
526
- return [
687
+ tools = [
688
+ # Skills management tools
689
+ Tool(
690
+ name="list_skills",
691
+ description=(
692
+ "List all available skills in the skills directory. "
693
+ "Returns skill names, descriptions, and languages."
694
+ ),
695
+ inputSchema={
696
+ "type": "object",
697
+ "properties": {},
698
+ "required": []
699
+ }
700
+ ),
701
+ Tool(
702
+ name="get_skill_info",
703
+ description=(
704
+ "Get detailed information about a specific skill, "
705
+ "including its input schema, description, and usage."
706
+ ),
707
+ inputSchema={
708
+ "type": "object",
709
+ "properties": {
710
+ "skill_name": {
711
+ "type": "string",
712
+ "description": "Name of the skill to get info for"
713
+ }
714
+ },
715
+ "required": ["skill_name"]
716
+ }
717
+ ),
718
+ Tool(
719
+ name="run_skill",
720
+ description=(
721
+ "Execute a skill with the given input parameters. "
722
+ "Use list_skills to see available skills and "
723
+ "get_skill_info to understand required parameters. "
724
+ "IMPORTANT: If the skill has high-severity security issues, "
725
+ "you MUST show the security report to the user and ASK for their explicit confirmation "
726
+ "before setting confirmed=true. Do NOT auto-confirm without user approval."
727
+ ),
728
+ inputSchema={
729
+ "type": "object",
730
+ "properties": {
731
+ "skill_name": {
732
+ "type": "string",
733
+ "description": "Name of the skill to execute"
734
+ },
735
+ "input": {
736
+ "type": "object",
737
+ "description": "Input parameters for the skill"
738
+ },
739
+ "confirmed": {
740
+ "type": "boolean",
741
+ "description": (
742
+ "Set to true ONLY after the user has explicitly approved execution. "
743
+ "You must ask the user for confirmation first."
744
+ )
745
+ },
746
+ "scan_id": {
747
+ "type": "string",
748
+ "description": "Scan ID from security review (required when confirmed=true)"
749
+ }
750
+ },
751
+ "required": ["skill_name"]
752
+ }
753
+ ),
754
+ # Code execution tools
527
755
  Tool(
528
756
  name="scan_code",
529
757
  description=(
@@ -551,8 +779,9 @@ class MCPServer:
551
779
  name="execute_code",
552
780
  description=(
553
781
  "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."
782
+ "IMPORTANT: If security issues are found, you MUST show the security report "
783
+ "to the user and ASK for their explicit confirmation before setting confirmed=true. "
784
+ "Do NOT auto-confirm without user approval."
556
785
  ),
557
786
  inputSchema={
558
787
  "type": "object",
@@ -569,8 +798,8 @@ class MCPServer:
569
798
  "confirmed": {
570
799
  "type": "boolean",
571
800
  "description": (
572
- "Set to true to confirm execution despite security warnings. "
573
- "Required when high-severity issues are found."
801
+ "Set to true ONLY after the user has explicitly approved execution. "
802
+ "You must ask the user for confirmation first."
574
803
  ),
575
804
  "default": False
576
805
  },
@@ -595,13 +824,22 @@ class MCPServer:
595
824
  }
596
825
  )
597
826
  ]
827
+ return tools
598
828
 
599
829
  @self.server.call_tool()
600
830
  async def call_tool(
601
831
  name: str,
602
832
  arguments: Dict[str, Any]
603
833
  ) -> "CallToolResult":
604
- if name == "scan_code":
834
+ # Skills management tools
835
+ if name == "list_skills":
836
+ return await self._handle_list_skills(arguments)
837
+ elif name == "get_skill_info":
838
+ return await self._handle_get_skill_info(arguments)
839
+ elif name == "run_skill":
840
+ return await self._handle_run_skill(arguments)
841
+ # Code execution tools
842
+ elif name == "scan_code":
605
843
  return await self._handle_scan_code(arguments)
606
844
  elif name == "execute_code":
607
845
  return await self._handle_execute_code(arguments)
@@ -675,6 +913,18 @@ class MCPServer:
675
913
  sandbox_level=sandbox_level,
676
914
  )
677
915
 
916
+ # Handle hard blocked case - this is a definitive block, not a confirmation request
917
+ if result.get("hard_blocked"):
918
+ return CallToolResult(
919
+ isError=True,
920
+ content=[
921
+ TextContent(
922
+ type="text",
923
+ text=result["stderr"]
924
+ )
925
+ ]
926
+ )
927
+
678
928
  if result.get("requires_confirmation"):
679
929
  return CallToolResult(
680
930
  content=[
@@ -684,15 +934,15 @@ class MCPServer:
684
934
  )
685
935
  ]
686
936
  )
687
-
937
+
688
938
  output_lines = []
689
939
  if result["stdout"]:
690
940
  output_lines.append(f"Output:\n{result['stdout']}")
691
941
  if result["stderr"]:
692
942
  output_lines.append(f"Errors:\n{result['stderr']}")
693
-
943
+
694
944
  output_text = "\n".join(output_lines) if output_lines else "Execution completed successfully (no output)"
695
-
945
+
696
946
  if result["success"]:
697
947
  return CallToolResult(
698
948
  content=[
@@ -712,7 +962,245 @@ class MCPServer:
712
962
  )
713
963
  ]
714
964
  )
715
-
965
+
966
+ async def _handle_list_skills(self, arguments: Dict[str, Any]) -> "CallToolResult":
967
+ """Handle list_skills tool call."""
968
+ if not self.skill_manager:
969
+ return CallToolResult(
970
+ content=[
971
+ TextContent(
972
+ type="text",
973
+ text=f"No skills available. Skills directory not found: {self.skills_dir}"
974
+ )
975
+ ]
976
+ )
977
+
978
+ skills = self.skill_manager.list_skills()
979
+ if not skills:
980
+ return CallToolResult(
981
+ content=[
982
+ TextContent(
983
+ type="text",
984
+ text=f"No skills found in directory: {self.skills_dir}"
985
+ )
986
+ ]
987
+ )
988
+
989
+ # Format skills list
990
+ lines = ["Available Skills:", ""]
991
+ for skill in skills:
992
+ lines.append(f"• **{skill.name}**")
993
+ if skill.description:
994
+ lines.append(f" {skill.description}")
995
+ if skill.language:
996
+ lines.append(f" Language: {skill.language}")
997
+ lines.append("")
998
+
999
+ return CallToolResult(
1000
+ content=[
1001
+ TextContent(
1002
+ type="text",
1003
+ text="\n".join(lines)
1004
+ )
1005
+ ]
1006
+ )
1007
+
1008
+ async def _handle_get_skill_info(self, arguments: Dict[str, Any]) -> "CallToolResult":
1009
+ """Handle get_skill_info tool call."""
1010
+ skill_name = arguments.get("skill_name")
1011
+
1012
+ if not skill_name:
1013
+ return CallToolResult(
1014
+ isError=True,
1015
+ content=[
1016
+ TextContent(
1017
+ type="text",
1018
+ text="Missing required argument: skill_name"
1019
+ )
1020
+ ]
1021
+ )
1022
+
1023
+ if not self.skill_manager:
1024
+ return CallToolResult(
1025
+ isError=True,
1026
+ content=[
1027
+ TextContent(
1028
+ type="text",
1029
+ text=f"No skills available. Skills directory not found: {self.skills_dir}"
1030
+ )
1031
+ ]
1032
+ )
1033
+
1034
+ skill = self.skill_manager.get_skill(skill_name)
1035
+ if not skill:
1036
+ available = ", ".join(self.skill_manager.skill_names()) or "none"
1037
+ return CallToolResult(
1038
+ isError=True,
1039
+ content=[
1040
+ TextContent(
1041
+ type="text",
1042
+ text=f"Skill not found: {skill_name}\nAvailable skills: {available}"
1043
+ )
1044
+ ]
1045
+ )
1046
+
1047
+ # Format skill info
1048
+ lines = [f"# {skill.name}", ""]
1049
+ if skill.description:
1050
+ lines.append(skill.description)
1051
+ lines.append("")
1052
+
1053
+ lines.append("## Details")
1054
+ lines.append(f"- **Language**: {skill.language or 'unknown'}")
1055
+ lines.append(f"- **Path**: {skill.path}")
1056
+ if skill.entry_point:
1057
+ lines.append(f"- **Entry Point**: {skill.entry_point}")
1058
+
1059
+ # Add input schema if available
1060
+ if skill.input_schema:
1061
+ lines.append("")
1062
+ lines.append("## Input Schema")
1063
+ lines.append("```json")
1064
+ lines.append(json.dumps(skill.input_schema, indent=2))
1065
+ lines.append("```")
1066
+
1067
+ return CallToolResult(
1068
+ content=[
1069
+ TextContent(
1070
+ type="text",
1071
+ text="\n".join(lines)
1072
+ )
1073
+ ]
1074
+ )
1075
+
1076
+ async def _handle_run_skill(self, arguments: Dict[str, Any]) -> "CallToolResult":
1077
+ """
1078
+ Handle run_skill tool call using UnifiedExecutionService.
1079
+
1080
+ This method uses the unified execution layer which:
1081
+ 1. Reads sandbox level at runtime
1082
+ 2. Handles security scanning
1083
+ 3. Properly downgrades sandbox level after confirmation
1084
+ """
1085
+ skill_name = arguments.get("skill_name")
1086
+ input_data = arguments.get("input", {})
1087
+ confirmed = arguments.get("confirmed", False)
1088
+ scan_id = arguments.get("scan_id")
1089
+
1090
+ if not skill_name:
1091
+ return CallToolResult(
1092
+ isError=True,
1093
+ content=[
1094
+ TextContent(
1095
+ type="text",
1096
+ text="Missing required argument: skill_name"
1097
+ )
1098
+ ]
1099
+ )
1100
+
1101
+ if not self.skill_manager:
1102
+ return CallToolResult(
1103
+ isError=True,
1104
+ content=[
1105
+ TextContent(
1106
+ type="text",
1107
+ text=f"No skills available. Skills directory not found: {self.skills_dir}"
1108
+ )
1109
+ ]
1110
+ )
1111
+
1112
+ if not self.skill_manager.has_skill(skill_name):
1113
+ available = ", ".join(self.skill_manager.skill_names()) or "none"
1114
+ return CallToolResult(
1115
+ isError=True,
1116
+ content=[
1117
+ TextContent(
1118
+ type="text",
1119
+ text=f"Skill not found: {skill_name}\nAvailable skills: {available}"
1120
+ )
1121
+ ]
1122
+ )
1123
+
1124
+ # Get skill info
1125
+ skill = self.skill_manager.get_skill(skill_name)
1126
+ if not skill:
1127
+ return CallToolResult(
1128
+ isError=True,
1129
+ content=[
1130
+ TextContent(
1131
+ type="text",
1132
+ text=f"Could not load skill: {skill_name}"
1133
+ )
1134
+ ]
1135
+ )
1136
+
1137
+ # Use UnifiedExecutionService
1138
+ from ..sandbox.execution_service import UnifiedExecutionService
1139
+ from ..sandbox.context import ExecutionContext
1140
+
1141
+ service = UnifiedExecutionService.get_instance()
1142
+
1143
+ # MCP uses async confirmation pattern (return report -> client calls back with confirmed=True)
1144
+ # Create a "callback" that captures the scan result for MCP's async flow
1145
+ scan_result_holder = {"result": None}
1146
+
1147
+ def mcp_confirmation_callback(report: str, scan_id_from_scan: str) -> bool:
1148
+ # If client already confirmed, return True
1149
+ if confirmed:
1150
+ return True
1151
+ # Otherwise, store the result and return False to abort execution
1152
+ # MCP will then return the report to the client
1153
+ scan_result_holder["result"] = {"report": report, "scan_id": scan_id_from_scan}
1154
+ return False
1155
+
1156
+ # Execute using unified service
1157
+ result = service.execute_skill(
1158
+ skill_info=skill,
1159
+ input_data=input_data,
1160
+ confirmation_callback=mcp_confirmation_callback if not confirmed else lambda r, s: True,
1161
+ )
1162
+
1163
+ # Check if we need to return a security report (MCP async confirmation pattern)
1164
+ if scan_result_holder["result"] is not None:
1165
+ report_data = scan_result_holder["result"]
1166
+ return CallToolResult(
1167
+ content=[
1168
+ TextContent(
1169
+ type="text",
1170
+ text=(
1171
+ f"🔐 Security Review Required for skill '{skill_name}'\n\n"
1172
+ f"{report_data['report']}\n\n"
1173
+ f"⚠️ IMPORTANT: You MUST ask the user for confirmation before proceeding.\n"
1174
+ f"Show this security report to the user and wait for their explicit approval.\n\n"
1175
+ f"If the user approves, call run_skill again with:\n"
1176
+ f" - confirmed: true\n"
1177
+ f" - scan_id: \"{report_data['scan_id']}\"\n"
1178
+ )
1179
+ )
1180
+ ]
1181
+ )
1182
+
1183
+ # Return execution result
1184
+ if result.success:
1185
+ return CallToolResult(
1186
+ content=[
1187
+ TextContent(
1188
+ type="text",
1189
+ text=f"Skill '{skill_name}' executed successfully.\n\nOutput:\n{result.output}"
1190
+ )
1191
+ ]
1192
+ )
1193
+ else:
1194
+ return CallToolResult(
1195
+ isError=True,
1196
+ content=[
1197
+ TextContent(
1198
+ type="text",
1199
+ text=f"Skill '{skill_name}' execution failed.\n\nError:\n{result.error}"
1200
+ )
1201
+ ]
1202
+ )
1203
+
716
1204
  async def run(self):
717
1205
  """Run the MCP server."""
718
1206
  async with stdio_server() as (read_stream, write_stream):