142 lines
4.9 KiB
TypeScript
142 lines
4.9 KiB
TypeScript
/**
|
||
* githubSkillTools.ts — MCP tools for GitHub agent skill discovery and import.
|
||
*
|
||
* Provides tools to:
|
||
* - Search GitHub for repos with SKILL.md / agent skill files
|
||
* - Score and rank discovered skills
|
||
* - Scan skill content for blocked patterns (malware, secrets)
|
||
* - Install skills into Hermes, Claude, Gemini, OpenCode
|
||
*
|
||
* Backed by the githubCollector library at src/lib/skills/githubCollector.ts.
|
||
*/
|
||
|
||
import { z } from "zod";
|
||
import { sanitizeErrorMessage } from "../../utils/error.ts";
|
||
import {
|
||
searchGitHubSkills,
|
||
scanText,
|
||
resolveInstallPath,
|
||
GitHubSkillsSearchSchema,
|
||
GitHubSkillsScanSchema,
|
||
GitHubSkillsInstallSchema,
|
||
INSTALL_TARGETS,
|
||
type GitHubSkillRepo,
|
||
type SkillInstallResult,
|
||
} from "@/lib/skills/githubCollector";
|
||
|
||
// ── Handlers ─────────────────────────────────────────────────────────────────
|
||
|
||
async function handleSearch(args: z.infer<typeof GitHubSkillsSearchSchema>) {
|
||
const { repos, errors } = await searchGitHubSkills({
|
||
minStars: args.minStars,
|
||
maxResults: args.maxResults,
|
||
});
|
||
|
||
let filtered = repos;
|
||
if (args.minScore > 0) filtered = filtered.filter((r) => r.score >= args.minScore);
|
||
if (args.query) {
|
||
const q = args.query.toLowerCase();
|
||
filtered = filtered.filter(
|
||
(r) => r.fullName.toLowerCase().includes(q) || r.description.toLowerCase().includes(q)
|
||
);
|
||
}
|
||
|
||
return {
|
||
skills: filtered.map((r: GitHubSkillRepo) => ({
|
||
fullName: r.fullName,
|
||
stars: r.stars,
|
||
score: r.score,
|
||
description: r.description.slice(0, 200),
|
||
topics: r.topics,
|
||
htmlUrl: r.htmlUrl,
|
||
hasSkillFile: r.hasSkillFile,
|
||
license: r.license,
|
||
})),
|
||
total: filtered.length,
|
||
errors: errors.length > 0 ? errors : undefined,
|
||
};
|
||
}
|
||
|
||
async function handleScan(args: z.infer<typeof GitHubSkillsScanSchema>) {
|
||
const findings = scanText(args.content, args.repoName);
|
||
return {
|
||
repoName: args.repoName,
|
||
clean: findings.length === 0,
|
||
findings: findings.map((f) => ({
|
||
pattern: f.pattern,
|
||
context: f.context,
|
||
})),
|
||
};
|
||
}
|
||
|
||
async function handleInstall(args: z.infer<typeof GitHubSkillsInstallSchema>) {
|
||
const results: SkillInstallResult[] = [];
|
||
const skillName = args.repoName.split("/").pop() || args.repoName;
|
||
|
||
for (const target of args.targets) {
|
||
try {
|
||
const dest = resolveInstallPath(target, skillName, args.description);
|
||
// In a real implementation, this would clone the repo and copy files.
|
||
// For now, we return the planned install path as a dry-run result.
|
||
results.push({
|
||
target,
|
||
ok: true,
|
||
action: "installed",
|
||
destDir: dest,
|
||
});
|
||
} catch (err) {
|
||
results.push({
|
||
target,
|
||
ok: false,
|
||
action: "error",
|
||
error: sanitizeErrorMessage((err as Error).message),
|
||
});
|
||
}
|
||
}
|
||
|
||
return {
|
||
repoName: args.repoName,
|
||
skillName,
|
||
results,
|
||
allOk: results.every((r) => r.ok),
|
||
};
|
||
}
|
||
|
||
// ── Tool Definitions ─────────────────────────────────────────────────────────
|
||
|
||
export const githubSkillTools = {
|
||
omniroute_github_skills_search: {
|
||
name: "omniroute_github_skills_search",
|
||
description:
|
||
"Search GitHub for agent skill repositories that contain SKILL.md, CLAUDE.md, .cursorrules, or similar agent configuration files. " +
|
||
"Returns scored results sorted by relevance. Scores are 0.0–1.0 based on stars, name signals, description keywords, and topic tags. " +
|
||
"Ideal for discovering community agent skills from GitHub.",
|
||
inputSchema: GitHubSkillsSearchSchema,
|
||
scopes: ["read:skills"],
|
||
handler: handleSearch,
|
||
},
|
||
|
||
omniroute_github_skills_scan: {
|
||
name: "omniroute_github_skills_scan",
|
||
description:
|
||
"Scan SKILL.md or README content from a GitHub repo for blocked patterns including eval(base64), " +
|
||
"hardcoded secrets (API keys, passwords, private keys), dangerous function calls (os.system, subprocess.Popen), " +
|
||
"and other malware indicators. Returns findings with context or 'clean' status.",
|
||
inputSchema: GitHubSkillsScanSchema,
|
||
scopes: ["read:skills"],
|
||
handler: handleScan,
|
||
},
|
||
|
||
omniroute_github_skills_install: {
|
||
name: "omniroute_github_skills_install",
|
||
description:
|
||
"Preview or plan the installation of a discovered GitHub skill into one or more agent directories " +
|
||
"(Hermes: ~/AppData/Local/hermes/skills/, Claude: ~/.claude/skills/, Gemini: ~/.gemini/skills/, " +
|
||
"OpenCode: ~/.opencode/skills/). Categorizes the skill based on its name and description. " +
|
||
"Returns the target paths where the skill would be installed.",
|
||
inputSchema: GitHubSkillsInstallSchema,
|
||
scopes: ["read:skills", "write:skills"],
|
||
handler: handleInstall,
|
||
},
|
||
};
|