Skip to content

Skill install needs reference resolution (matching thv run pattern) #4015

@JAORMX

Description

@JAORMX

Problem

thv skill install <name> with a plain name creates a "pending" record that never resolves to "installed". The only working install path today is with a fully-qualified OCI reference:

# This works
thv skill install ghcr.io/org/my-skill:v1

# These don't — skill stays "pending" forever
thv skill build ./my-skill && thv skill install my-skill
thv skill install some-published-skill

This breaks both the local development workflow (build → install) and the registry-based discovery workflow (install by name).

How thv run solves this for MCP servers

thv run has a cascading reference resolution model that skills should follow:

Reference type thv run example Resolution
Registry name thv run filesystem Looks up in registry → gets OCI image ref → pulls
Direct OCI ref thv run ghcr.io/org/server:v1 Pulls directly
Protocol scheme thv run uvx://mcp-server-git Builds from source template
Remote URL thv run https://api.example.com/mcp Proxies to remote

The resolution cascade in pkg/runner/retriever/retriever.go:

  1. Protocol scheme check (uvx://, npx://, go://)
  2. Registry lookup (provider.GetServer(name))
  3. Fallthrough to direct image reference

What thv skill install needs

Skills should support analogous reference types:

Reference type Example Resolution
Registry/index name thv skill install my-skill Look up in skill index/registry → get OCI ref → pull & extract
Direct OCI ref thv skill install ghcr.io/org/my-skill:v1 Pull & extract (already works)
Local build tag thv skill install my-skill (after thv skill build) Resolve from local OCI store → extract
Git reference thv skill install git://github.com/org/repo#path/to/skill Clone & extract (future)

Root cause

The Install method in pkg/skills/skillsvc/skillsvc.go has three paths:

  1. OCI reference (contains /, :, @) → installFromOCI() → pulls from remote → extracts → works
  2. Plain name + LayerDatainstallWithExtraction() → extracts → works (but unreachable from CLI/API)
  3. Plain name, no LayerDatainstallPending() → creates DB record with status: "pending"dead end

Path 3 is always hit for plain names because:

  • The CLI/API never sends LayerData (internal-only field)
  • No registry/index lookup exists for skills
  • No local OCI store lookup exists
  • Nothing ever transitions "pending" to "installed"

Proposed resolution cascade for thv skill install <name-or-ref>

1. Is it an OCI reference? (contains /, :, @)
   YES → installFromOCI (already implemented)

2. Is it in the local OCI store? (from a prior `thv skill build`)
   YES → extract from local store → status = "installed"

3. Is it in the skill registry/index?
   YES → get OCI reference → installFromOCI

4. Not found → return actionable error:
   "skill 'foo' not found. Use an OCI reference (ghcr.io/org/foo:v1)
    or build locally first (thv skill build ./foo)"

Step 3 can reuse the provider pattern from pkg/registry/SkillIndexEntry already has a Repository field for the OCI reference.

Existing infrastructure to reuse

  • pkg/registry/ provider pattern: Factory, base provider, API/local/remote provider implementations. The Provider interface with GetServer(name) maps directly to skill index lookup.
  • pkg/skills/types.go: SkillIndex and SkillIndexEntry types already defined with Repository (OCI ref) field
  • skillsvc.installFromOCI: Already handles pull → extract → validate → store for OCI references
  • Local OCI store (ociskills.Store): Already used by build and installFromOCI, just needs a resolve-by-tag path
  • toolhive-core/registry/types: Skill struct with Packages supporting both "oci" and "git" registry types

Additional tech debt: hand-rolled OCI tag validation

validateOCITag in skillsvc.go used a hand-rolled regex instead of the go-containerregistry library that's already imported. This is being fixed in #4010.

Suggested phased approach

Phase 1 — Local build → install (unblocks developer workflow):

  • When install gets a plain name, check local OCI store for matching tag
  • If found, extract from local store (reuse installFromOCI extraction logic)
  • Fix E2E lifecycle test to verify actual extraction

Phase 2 — Registry/index lookup (unblocks published skill discovery):

  • Add skill index provider (following pkg/registry/ provider pattern)
  • Plain name → index lookup → OCI reference → installFromOCI
  • Could back onto the same MCP Registry API or a dedicated skill index

Phase 3 — Git references (future):

  • Add git:// protocol scheme support for skills
  • Clone repo, locate skill directory, extract

Context

Discovered during review of #4010 — the E2E lifecycle test annotates install as "(pending)" without verifying actual extraction. Reviewer (@reyortiz3) flagged that install stays pending.

Metadata

Metadata

Assignees

No one assigned

    Labels

    cliChanges that impact CLI functionalityenhancementNew feature or requestgoPull requests that update go coderegistryskillsSkills lifecycle management

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions