Repository Public MIT
1import { CLIENT_LABELS, type ArtifactType, type Client } from "./constants";2import type { Manifest, McpBlock } from "./manifest";34/**5 * Installation adapters. Pure functions that DESCRIBE what to do; nothing here runs a package.6 * Paths and config formats were checked against each client's official documentation on 2026-11-097 * (see docs/STANDARDS.md). Download, configure and execute stay three separate user decisions.8 */910export type InstallScope = "user" | "project";1112export interface InstallStep {13 title: string;14 command?: string;15 note?: string;16}17export interface ConfigSnippet {18 file: string;19 language: "json" | "toml" | "bash";20 content: string;21}22export interface InstallInstructions {23 client: Client;24 clientLabel: string;25 supported: boolean;26 reason?: string;27 strategy: "copy-directory" | "mcp-config" | "manual";28 destination?: { user?: string; project?: string };29 cli?: string;30 steps: InstallStep[];31 config: ConfigSnippet[];32 warnings: string[];33 requiredEnv: { name: string; description?: string; required: boolean; secret: boolean }[];34}3536const SKILL_DIRS: Partial<Record<Client, { user: string; project: string }>> = {37 "claude-code": { user: "~/.claude/skills", project: ".claude/skills" },38 codex: { user: "~/.agents/skills", project: ".agents/skills" },39 cursor: { user: "~/.cursor/skills", project: ".cursor/skills" },40 "generic-agent-skills": { user: "~/.agents/skills", project: ".agents/skills" },41};4243export function skillDestination(client: Client, slug: string, scope: InstallScope): string | null {44 const d = SKILL_DIRS[client];45 return d ? `${d[scope]}/${slug}` : null;46}4748export interface InstallTarget {49 namespace: string;50 slug: string;51 version: string;52 type: ArtifactType;53 sha256: string;54 manifest: Pick<Manifest, "mcp" | "permissions">;55 baseUrl: string;56}5758export function downloadUrl(t: Pick<InstallTarget, "namespace" | "slug" | "version" | "baseUrl">): string {59 return `${t.baseUrl}/api/v1/artifacts/${t.namespace}/${t.slug}/versions/${t.version}/download`;60}6162function shq(s: string): string {63 return /^[A-Za-z0-9_@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;64}65function tomlStr(s: string): string {66 return JSON.stringify(s);67}6869function manualSteps(t: InstallTarget, dest: string): InstallStep[] {70 const file = `${t.slug}-${t.version}.tar.gz`;71 return [72 { title: "Download the release archive", command: `curl -fsSLo ${file} ${downloadUrl(t)}` },73 { title: "Verify the SHA-256 checksum", command: `echo "${t.sha256} ${file}" | shasum -a 256 -c -` },74 { title: "Inspect the contents before extracting", command: `tar -tzf ${file}` },75 { title: `Extract into ${dest}`, command: `mkdir -p ${dest} && tar -xzf ${file} -C ${dest}` },76 ];77}7879function mcpEnv(mcp: McpBlock) {80 return (mcp.env ?? []).map((e) => ({ name: e.name, description: e.description, required: e.required ?? true, secret: e.secret ?? false }));81}8283function mcpInstructions(t: InstallTarget, client: Client, mcp: McpBlock): Pick<InstallInstructions, "steps" | "config" | "warnings"> {84 const name = t.slug;85 const env = mcp.env ?? [];86 const steps: InstallStep[] = [];87 const config: ConfigSnippet[] = [];88 const warnings: string[] = [];89 if (mcp.transport === "stdio") {90 const command = mcp.command!;91 const args = mcp.args ?? [];92 warnings.push("This server runs locally on your machine with your user's privileges. Review the source before running it; the registry does not execute or host it.");93 if (client === "claude-code") {94 const envFlags = env.map((e) => `--env ${e.name}=<value>`).join(" ");95 steps.push({ title: "Register the local server", command: `claude mcp add --transport stdio ${envFlags ? envFlags + " " : ""}${shq(name)} -- ${[command, ...args].map(shq).join(" ")}` });96 config.push({ file: ".mcp.json (project scope)", language: "json", content: JSON.stringify({ mcpServers: { [name]: { command, args, ...(env.length ? { env: Object.fromEntries(env.map((e) => [e.name, `\${${e.name}}`])) } : {}) } } }, null, 2) });97 } else if (client === "codex") {98 let toml = `[mcp_servers.${/^[A-Za-z0-9_-]+$/.test(name) ? name : tomlStr(name)}]\ncommand = ${tomlStr(command)}\nargs = [${args.map(tomlStr).join(", ")}]\n`;99 if (env.length) toml += `env_vars = [${env.map((e) => tomlStr(e.name)).join(", ")}]\n`;100 config.push({ file: "~/.codex/config.toml", language: "toml", content: toml });101 steps.push({ title: "Add the server block to your Codex configuration", note: "env_vars forwards the named variables from your shell; set them locally. Codex asks before each tool call; when it runs with approval policy \"never\" it refuses MCP tool calls unless the server block also has default_tools_approval_mode = \"approve\" (observed with Codex CLI 0.167)." });102 } else if (client === "cursor") {103 config.push({ file: ".cursor/mcp.json (project) or ~/.cursor/mcp.json (global)", language: "json", content: JSON.stringify({ mcpServers: { [name]: { type: "stdio", command, args, ...(env.length ? { env: Object.fromEntries(env.map((e) => [e.name, `\${env:${e.name}}`])) } : {}) } } }, null, 2) });104 steps.push({ title: "Add the server to your Cursor MCP configuration" });105 } else {106 steps.push({ title: "Start the server over stdio from your MCP client", command: [command, ...args].map(shq).join(" ") });107 }108 } else {109 const url = mcp.url!;110 warnings.push("This is a remote endpoint operated by the publisher, not by this registry. Requests and any data you send go to that operator.");111 if (client === "claude-code") {112 steps.push({ title: "Register the remote server", command: `claude mcp add --transport http ${shq(name)} ${shq(url)}` });113 config.push({ file: ".mcp.json (project scope)", language: "json", content: JSON.stringify({ mcpServers: { [name]: { type: "http", url } } }, null, 2) });114 } else if (client === "codex") {115 config.push({ file: "~/.codex/config.toml", language: "toml", content: `[mcp_servers.${/^[A-Za-z0-9_-]+$/.test(name) ? name : tomlStr(name)}]\nurl = ${tomlStr(url)}\n` });116 steps.push({ title: "Add the server block to your Codex configuration" });117 } else if (client === "cursor") {118 config.push({ file: ".cursor/mcp.json (project) or ~/.cursor/mcp.json (global)", language: "json", content: JSON.stringify({ mcpServers: { [name]: { url } } }, null, 2) });119 steps.push({ title: "Add the server to your Cursor MCP configuration" });120 } else {121 steps.push({ title: "Connect your MCP client to the Streamable HTTP endpoint", note: url });122 }123 }124 if (env.length) steps.push({ title: "Set the required environment variables locally", note: `${env.map((e) => e.name).join(", ")} — configured on your machine only; never sent to the registry.` });125 return { steps, config, warnings };126}127128export function installInstructions(t: InstallTarget, client: Client): InstallInstructions {129 const ref = `@${t.namespace}/${t.slug}@${t.version}`;130 const base: InstallInstructions = { client, clientLabel: CLIENT_LABELS[client], supported: true, strategy: "manual", steps: [], config: [], warnings: [], requiredEnv: [] };131 const perms = t.manifest.permissions ?? [];132 if (perms.length) base.warnings.push(`Declared permissions: ${perms.join(", ")}.`);133134 if (t.type === "skill") {135 if (client === "generic-mcp") return { ...base, supported: false, reason: "Agent Skills are not MCP servers; pick a Skills-compatible client." };136 const dirs = SKILL_DIRS[client]!;137 const user = `${dirs.user}/${t.slug}`;138 const project = `${dirs.project}/${t.slug}`;139 return {140 ...base,141 strategy: "copy-directory",142 destination: { user, project },143 cli: `skills install ${ref} --client ${client}`,144 steps: [145 { title: "Install with the Skills CLI (verifies the checksum, previews the destination, never runs scripts)", command: `skills install ${ref} --client ${client}`, note: `Add --project to install into ${project} instead of ${user}.` },146 ...manualSteps(t, user).map((s, i) => (i === 0 ? { ...s, title: `Or manually: ${s.title.toLowerCase()}` } : s)),147 ],148 warnings: [...base.warnings, "Skills can contain scripts. Installing only copies files; read SKILL.md and any scripts before letting an agent use them."],149 };150 }151152 if (t.type === "mcp-server") {153 if (client === "generic-agent-skills") return { ...base, supported: false, reason: "MCP servers are configured in an MCP client, not installed as a skill directory." };154 const mcp = t.manifest.mcp;155 if (!mcp) return { ...base, supported: false, reason: "This release does not declare its MCP transport." };156 const r = mcpInstructions(t, client, mcp);157 const src = mcp.transport === "stdio" ? [{ title: "Optional: download the listing's source archive for review", command: `skills download ${ref}` }] : [];158 return { ...base, strategy: "mcp-config", steps: [...r.steps, ...src], config: r.config, warnings: [...base.warnings, ...r.warnings], requiredEnv: mcpEnv(mcp) };159 }160161 if (client === "generic-mcp") return { ...base, supported: false, reason: `A ${t.type} is not an MCP server.` };162 const dest = `./${t.slug}`;163 return {164 ...base,165 strategy: "manual",166 destination: { project: dest },167 cli: `skills download ${ref}`,168 steps: [{ title: "Download and verify with the Skills CLI", command: `skills download ${ref} --extract ${dest}` }, ...manualSteps(t, dest).map((s, i) => (i === 0 ? { ...s, title: `Or manually: ${s.title.toLowerCase()}` } : s))],169 warnings: [...base.warnings, `No client-specific installer exists for type "${t.type}"; follow the package README.`],170 };171}172