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:
- Protocol scheme check (
uvx://, npx://, go://)
- Registry lookup (
provider.GetServer(name))
- 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:
- OCI reference (contains
/, :, @) → installFromOCI() → pulls from remote → extracts → works
- Plain name + LayerData →
installWithExtraction() → extracts → works (but unreachable from CLI/API)
- Plain name, no LayerData →
installPending() → 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.
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 breaks both the local development workflow (build → install) and the registry-based discovery workflow (install by name).
How
thv runsolves this for MCP serversthv runhas a cascading reference resolution model that skills should follow:thv runexamplethv run filesystemthv run ghcr.io/org/server:v1thv run uvx://mcp-server-gitthv run https://api.example.com/mcpThe resolution cascade in
pkg/runner/retriever/retriever.go:uvx://,npx://,go://)provider.GetServer(name))What
thv skill installneedsSkills should support analogous reference types:
thv skill install my-skillthv skill install ghcr.io/org/my-skill:v1thv skill install my-skill(afterthv skill build)thv skill install git://github.com/org/repo#path/to/skillRoot cause
The
Installmethod inpkg/skills/skillsvc/skillsvc.gohas three paths:/,:,@) →installFromOCI()→ pulls from remote → extracts → worksinstallWithExtraction()→ extracts → works (but unreachable from CLI/API)installPending()→ creates DB record withstatus: "pending"→ dead endPath 3 is always hit for plain names because:
LayerData(internal-only field)Proposed resolution cascade for
thv skill install <name-or-ref>Step 3 can reuse the provider pattern from
pkg/registry/—SkillIndexEntryalready has aRepositoryfield for the OCI reference.Existing infrastructure to reuse
pkg/registry/provider pattern: Factory, base provider, API/local/remote provider implementations. TheProviderinterface withGetServer(name)maps directly to skill index lookup.pkg/skills/types.go:SkillIndexandSkillIndexEntrytypes already defined withRepository(OCI ref) fieldskillsvc.installFromOCI: Already handles pull → extract → validate → store for OCI referencesociskills.Store): Already used bybuildandinstallFromOCI, just needs a resolve-by-tag pathtoolhive-core/registry/types: Skill struct withPackagessupporting both "oci" and "git" registry typesAdditional tech debt: hand-rolled OCI tag validation
validateOCITaginskillsvc.goused a hand-rolled regex instead of thego-containerregistrylibrary that's already imported. This is being fixed in #4010.Suggested phased approach
Phase 1 — Local build → install (unblocks developer workflow):
installgets a plain name, check local OCI store for matching taginstallFromOCIextraction logic)Phase 2 — Registry/index lookup (unblocks published skill discovery):
pkg/registry/provider pattern)installFromOCIPhase 3 — Git references (future):
git://protocol scheme support for skillsContext
Discovered during review of #4010 — the E2E lifecycle test annotates install as "(pending)" without verifying actual extraction. Reviewer (@reyortiz3) flagged that install stays pending.