Coverage for mcp_bridge/tools/skill_loader.py: 10%

83 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-01-10 00:20 -0500

1""" 

2Skill Loader - Claude Code Slash Command Discovery 

3 

4Discovers and lists available skills (slash commands) from: 

51. Project-local .claude/commands/ 

62. User-global ~/.claude/commands/ 

7 

8Skills are markdown files with frontmatter defining the command behavior. 

9""" 

10 

11import re 

12from pathlib import Path 

13from typing import Any 

14 

15 

16def parse_frontmatter(content: str) -> tuple[dict[str, Any], str]: 

17 """ 

18 Parse YAML frontmatter from markdown content. 

19 

20 Returns: 

21 Tuple of (metadata dict, body content) 

22 """ 

23 if not content.startswith("---"): 

24 return {}, content 

25 

26 # Find the closing --- 

27 end_match = content.find("---", 3) 

28 if end_match == -1: 

29 return {}, content 

30 

31 frontmatter = content[3:end_match].strip() 

32 body = content[end_match + 3 :].strip() 

33 

34 # Simple YAML parsing for key: value pairs 

35 metadata = {} 

36 for line in frontmatter.split("\n"): 

37 if ":" in line: 

38 key, _, value = line.partition(":") 

39 key = key.strip() 

40 value = value.strip().strip('"').strip("'") 

41 metadata[key] = value 

42 

43 return metadata, body 

44 

45 

46def discover_skills(project_path: str | None = None) -> list[dict[str, Any]]: 

47 """ 

48 Discover available skills/commands. 

49 

50 Searches: 

51 1. Project-local: {project}/.claude/commands/ 

52 2. User-global: ~/.claude/commands/ 

53 

54 Args: 

55 project_path: Project directory to search (defaults to cwd) 

56 

57 Returns: 

58 List of skill definitions. 

59 """ 

60 skills = [] 

61 search_paths = [] 

62 

63 # Project-local commands 

64 project = Path(project_path) if project_path else Path.cwd() 

65 project_commands = project / ".claude" / "commands" 

66 if project_commands.exists(): 

67 search_paths.append(("project", project_commands)) 

68 

69 # User-global commands 

70 user_commands = Path.home() / ".claude" / "commands" 

71 if user_commands.exists(): 

72 search_paths.append(("user", user_commands)) 

73 

74 for scope, commands_dir in search_paths: 

75 for md_file in commands_dir.glob("**/*.md"): 

76 try: 

77 content = md_file.read_text() 

78 metadata, body = parse_frontmatter(content) 

79 

80 skills.append( 

81 { 

82 "name": md_file.stem, 

83 "scope": scope, 

84 "path": str(md_file), 

85 "description": metadata.get("description", ""), 

86 "allowed_tools": metadata.get("allowed-tools", "").split(",") 

87 if metadata.get("allowed-tools") 

88 else [], 

89 "body_preview": body[:200] + "..." if len(body) > 200 else body, 

90 } 

91 ) 

92 except Exception: 

93 continue 

94 

95 return skills 

96 

97 

98def list_skills(project_path: str | None = None) -> str: 

99 """ 

100 List all available skills for MCP tool. 

101 

102 Args: 

103 project_path: Project directory to search 

104 

105 Returns: 

106 Formatted skill listing. 

107 """ 

108 skills = discover_skills(project_path) 

109 

110 if not skills: 

111 return "No skills found. Create .claude/commands/*.md files to add skills." 

112 

113 lines = [f"Found {len(skills)} skill(s):\n"] 

114 

115 for skill in skills: 

116 scope_badge = "[project]" if skill["scope"] == "project" else "[user]" 

117 lines.append(f" /{skill['name']} {scope_badge}") 

118 if skill["description"]: 

119 lines.append(f" {skill['description']}") 

120 

121 return "\n".join(lines) 

122 

123 

124def get_skill(name: str, project_path: str | None = None) -> str: 

125 """ 

126 Get the content of a specific skill. 

127 

128 Args: 

129 name: Skill name (filename without .md) 

130 project_path: Project directory to search 

131 

132 Returns: 

133 Skill content or error message. 

134 """ 

135 skills = discover_skills(project_path) 

136 

137 skill = next((s for s in skills if s["name"] == name), None) 

138 if not skill: 

139 available = ", ".join(s["name"] for s in skills) 

140 return f"Skill '{name}' not found. Available: {available or 'none'}" 

141 

142 try: 

143 content = Path(skill["path"]).read_text() 

144 metadata, body = parse_frontmatter(content) 

145 

146 lines = [ 

147 f"## Skill: {name}", 

148 f"**Scope**: {skill['scope']}", 

149 f"**Path**: {skill['path']}", 

150 ] 

151 

152 if metadata.get("description"): 

153 lines.append(f"**Description**: {metadata['description']}") 

154 

155 if metadata.get("allowed-tools"): 

156 lines.append(f"**Allowed Tools**: {metadata['allowed-tools']}") 

157 

158 lines.extend(["", "---", "", body]) 

159 

160 return "\n".join(lines) 

161 

162 except Exception as e: 

163 return f"Error reading skill: {e}" 

164 

165 

166def create_skill( 

167 name: str, 

168 description: str, 

169 content: str, 

170 scope: str = "project", 

171 project_path: str | None = None, 

172) -> str: 

173 """ 

174 Create a new skill file. 

175 

176 Args: 

177 name: Skill name (will be used as filename) 

178 description: Short description for frontmatter 

179 content: Skill body content 

180 scope: "project" or "user" 

181 project_path: Project directory for project-scope skills 

182 

183 Returns: 

184 Success or error message. 

185 """ 

186 # Sanitize name 

187 name = re.sub(r"[^a-zA-Z0-9_-]", "-", name.lower()) 

188 

189 if scope == "project": 

190 base_dir = Path(project_path) if project_path else Path.cwd() 

191 commands_dir = base_dir / ".claude" / "commands" 

192 else: 

193 commands_dir = Path.home() / ".claude" / "commands" 

194 

195 # Ensure directory exists 

196 commands_dir.mkdir(parents=True, exist_ok=True) 

197 

198 skill_path = commands_dir / f"{name}.md" 

199 

200 if skill_path.exists(): 

201 return f"Skill '{name}' already exists at {skill_path}" 

202 

203 # Create skill content 

204 skill_content = f"""--- 

205description: {description} 

206--- 

207 

208{content} 

209""" 

210 

211 try: 

212 skill_path.write_text(skill_content) 

213 return f"Created skill '{name}' at {skill_path}" 

214 except Exception as e: 

215 return f"Error creating skill: {e}"