Files
OpenViking/examples
t0saki 6b127eb92c feat(mcp): add an add_skill tool and make skills findable over MCP (#5160)
* refactor(skills): install skills through one shared helper

POST /api/v1/skills kept its whole install loop (source resolution, per-skill
install, source metadata, list_only) inline in the route. Move it into
openviking/server/skill_ingest.py:install_skills so the MCP add_skill tool
and signed skill uploads can reuse the exact same code path. The REST
route's behavior is unchanged.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(mcp): add an add_skill tool

MCP clients had no way to create a skill: write refuses the skills/
subtree (_USER_MANAGED_SUBTREES) and add_resource validates its target as
a resource. Agents that should keep skills in OpenViking could read them
but never add one.

add_skill takes either the full SKILL.md text (data) or a path. A Git or
GitHub tree URL installs through the same source resolution as REST, with
skills=[...] to pick from a multi-skill repository and list_only to
preview it. A local SKILL.md, directory, or zip gets the add_resource
treatment: the tool mints a one-time upload token, now tagged kind="skill"
with the target root, selection and list_only, and the signed temp_upload
installs the file as skills instead of ingesting it as a resource.
target_uri="viking://agent/skills" shares the skill with the account.

All three paths (REST, MCP inline/Git, signed upload) go through
skill_ingest.install_skills. The tool count in the server log, app
comment, docs, and the Codex plugin's REAL_MCP_TOOLS moves to 16.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): search shared skills in find(context_type="skill")

Without a target_uri, find resolved the generic default targets, which
stop at the caller's user root, so a skill search never reached the
account-shared viking://agent/skills. REST /skills/find and the context
search already cover both roots. When context_type resolves to skill only
and no target_uri is given, the MCP tool now targets
default_target_directories(ctx, context_type=SKILL): the user's own skills
plus viking://agent/skills. REST find semantics are unchanged.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): print directory abstracts in tree(include_abstract=true)

The tree tool skipped to the next entry right after printing a directory,
and only printed abstracts for files, but the storage layer only fills
abstracts for directories (files always come back empty). The flag
therefore never printed anything. Print the abstract after either kind
of entry and ask for up to 1024 characters, enough for a full skill
description, so tree(uri="viking://~/skills", level_limit=1,
include_abstract=true) lists every skill with its description.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(skills): honor node_limit in GET /api/v1/skills

list_skills declared node_limit but always listed each skill root with a
hardcoded 1000. Pass it through per root; 0 keeps the default so the CLI's
accepted range (-n 0) still lists everything.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): point skill hits in find/search at their SKILL.md

A skill is indexed through its directory's .abstract.md, so find and
list-mode search printed hits like viking://agent/skills/x/.abstract.md.
Following the "use the read tool to expand a URI" advice returned only
the frontmatter, and read_content inlined the same stub. Skill hits now
show <dir>/SKILL.md, and read_content reads that file.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): validate add_skill targets and sources before minting an upload

Review findings on the add_skill tool:

- target_uri passed the content-kind check for any path under a skills
  root (viking://~/skills/pdf) and, for ROOT, for another user's root,
  but the installer only accepts the caller's own skills root or
  viking://agent/skills. On the local-path branch the tool minted a
  one-time upload token anyway, and the upload failed with 400 after the
  token was spent. The target is now resolved with the installer's own
  rule first; shared subpaths map to viking://agent/skills, the rest fail
  at once, and the error names both allowed roots.
- Non-Git remote sources such as tos:// were treated as remote, then
  refused as "direct host filesystem paths". add_skill now decides Git
  with the same prefixes resolve_skill_source uses (shared as
  GIT_SKILL_SOURCE_PREFIXES) and reports other schemes as unsupported.
- With list_only, the upload instructions still said the skill would be
  installed and that no further call was needed; they now say the upload
  only lists the source's skills.
- The zip example packaged hidden files, so .git and .env files went
  into the stored skill. It now excludes VCS data, .env files,
  node_modules and .DS_Store, starting from a fresh archive.
- tree(include_abstract=true) printed the "abstract is not ready"
  placeholder for directories that never get an abstract, such as a
  skill's scripts/. Those placeholders are skipped.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* docs(mcp): say that write only refuses the user's own skills subtree

The capability reference claimed MCP write refuses every skill URI. It
refuses the user's own skills/ subtree, but under viking://agent/skills
it writes a plain file that skips skill installation. State that, and
point shared skills at add_skill as well.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* style(skills): format skill_processor.py

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): return one hit per skill package in find

#5045 made a skill index as a whole package, so an item-level find now
returns one hit per file inside it. Route skill-only find through
SearchService.find_skills, which keeps the best hit per package, and
resolve every skill hit to its package's SKILL.md instead of only
rewriting the two index sidecars.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): point skill changes at add_skill in the tool descriptions

The server keeps accepting write/edit under viking://agent/skills, and
forget still removes a skill directory, so the constraint lives in the
tool descriptions: add_skill is the one entry point for creating and
updating a skill, and removal goes through ov skills remove or Studio.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* fix(mcp): describe a skill hit by its own abstract

A package hit can be any file inside the skill, whose abstract describes
that file and not the skill, so find would list a skill under a helper
script's summary. Read the package's abstract for those hits, the way
GET /skills/find already does. Keep a filter-only skill query on the
generic find, which find_skills does not serve.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): document package-level skill retrieval in find

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* fix(mcp): do not paste an unready abstract over a skill hit's own summary

fs.abstract returns a placeholder string rather than raising when a package
has no usable .abstract.md, so the substitution replaced a useful file
summary with a diagnostic line. Reject the same placeholders tree already
rejects, and bound the per-package reads the way read_content is bounded.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): correct how search reports skill hits, and refresh the tool table

find and search both render one line per skill package, so the earlier
wording — that search returns several hits per package — contradicted the
code. Say what actually differs: search still spends a limit slot per
matching file and keeps that file's summary. Also point forget and
add_resource at add_skill where an agent would look for them, name the REST
delete alongside the CLI, and bring the capability table's line citations
back in step with mcp_endpoint.py.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* refactor(mcp): drop guards and prose no caller can reach

install_skills only ever returns a dict, add_skill always fills root_uri,
and a source with no SKILL.md raises before it gets here, so the
isinstance, empty-list and missing-uri branches were unreachable.
fs.abstract only returns the directory placeholder. One skill package
resolves to one rendered item, so the pending map holds one each. In the
docstrings, drop what Args already says and the one removal path an MCP
caller cannot take.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): say what list-mode search actually reports for a skill hit

The search row claimed a skill package's summary is the matching file's.
It is not: _format_search_result rewrites every skill hit onto the
package's SKILL.md, keeps the best-scored one per package, and
_describe_skills_by_package replaces the summary with the package's own
abstract. What is true is that limit applies during retrieval, before
that merge, so a package matching several files still spends several
slots and fewer than limit results come back.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW
2026-09-22 14:49:20 +08:00
..