Files
OmniRoute/open-sse/mcp-server/tools/githubSkillTools.ts
Moseyuh333 b9d18dd8c4 Continue fix bugs and upgrade skill_collector (#6294)
* fix(skills): gate skill-collector CLI detection behind management auth + loopback

PR #6294 fork-main bundled genuinely new skill-collector CLI-detection routes
(GET /api/skills/collect/detect, POST /api/skills/collect/install) on top of
content already shipped via #6186. This reconstructs the PR against the
current release tip, keeping only the new detect/install routes and their
SKILL.md, and drops the 3 already-merged commits so two post-merge quality
fixes on /api/github-skills (Zod validation + sanitizeErrorMessage) are not
reverted.

- GET /api/skills/collect/detect spawned a child process per CLI_TOOL_IDS
  entry via getCliRuntimeStatus(), unauthenticated and reachable over any
  tunnel. All 3 routes (github-skills GET/POST, skills/collect/detect,
  skills/collect/install) now require requireManagementAuth(), matching
  every sibling /api/skills/* route.
- Classified /api/skills/collect/ in LOCAL_ONLY_API_PREFIXES and
  SPAWN_CAPABLE_PREFIXES (routeGuard.ts / spawnCapablePrefixes.ts) and added
  src/app/api/skills/collect to SPAWN_CAPABLE_ROUTE_ROOTS in
  check-route-guard-membership.ts so the automated gate actually scans it
  (Hard Rules #15 + #17).
- omniroute_github_skills_install MCP tool now reports the honest
  action: "planned" instead of "installed", matching the REST route.
- Dropped docker-compose.drive-d.yml, start.sh, and the unrelated
  @types/node/settings.ts changes (personal dev-machine / out-of-scope).
- Added route-level tests for all 3 routes + the 3 MCP tools (auth-required
  and no-stack-trace-leak assertions) and a route-guard regression test.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(quality): register new routeGuard covering test in stryker.conf.json

check:mutation-test-coverage --strict (Fast Quality Gates) flagged
tests/unit/authz/route-guard-skills-collect.test.ts as a covering unit test
for src/server/authz/routeGuard.ts that was missing from tap.testFiles, so
its mutant kills would silently not count toward the mutation-test baseline.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore: resync CHANGELOG after merging release/v3.8.47

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Moseyuh333 <Moseyuh333@users.noreply.github.com>
2026-07-09 19:43:43 -03:00

144 lines
5.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 — matches
// the honest `action: "planned"` the REST route (/api/github-skills POST)
// reports for the same operation.
results.push({
target,
ok: true,
action: "planned",
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.01.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,
},
};