Skip to content

Public Python API for the bundled catalogue and skill folders (for rf-mcp) #5

Closed

Description

@manykarim

Summary

Give the robotframework-plugins package a small, supported Python API for the plugin content it already bundles: the catalogue, and the folders holding each plugin's skills. Then other tools can depend on the package and read that content without importing private modules.

Purpose

rf-mcp (the Robot Framework MCP server) plans to stop shipping its own library skills (locators, Browser, Selenium, RequestsLibrary, …) and serve the official skills from this repository instead. On top of that it would add library-aware retrieval: rf-mcp knows which libraries and versions a project uses, and which error just happened, so it can point the agent at the matching official skill section at the moment it's needed.

To do that, rf-mcp would declare robotframework-plugins as a dependency, so uv tool install rf-mcp brings the official skills along without a separate install step or network access at runtime. rf-mcp would then need:

  1. The catalogue: plugin names, versions, libraries[{name, package, versions}] and the robotframework range, to match a project's installed packages against the plugins.
  2. The skill folders: the skills/ folder of each bundled plugin as a real filesystem path. rf-mcp serves skills from directories (FastMCP's SkillsDirectoryProvider), with a project's own skills and a company's --skills-dir overriding them by name.
  3. A stable contract: a version range of robotframework-plugins that rf-mcp can pin, and a guarantee that the shape it reads won't change within that range.

Today everything needed exists, but it is private: robotframework_plugins._assets (bundled_catalog(), asset_root_path(), content_plugins()), and __all__ exports only __version__. _assets/catalog.json has no format version.

Proposed API

A new public module robotframework_plugins.bundle. Names are only suggestions:

from robotframework_plugins import bundle

bundle.CATALOG_FORMAT        # int, 1. Bumped only on a breaking change to catalog()'s entries.

bundle.catalog() -> list[dict]
# The bundled plugins (status active or deprecated), as in _assets/catalog.json.
# Each entry has at least: name, version, status, tier, summary,
# libraries [{name, package, versions}], robotframework, bundle/bundled.

bundle.skills_dirs() -> dict[str, pathlib.Path]
# {plugin name: absolute path of its skills/ folder} for every active plugin with content
# (bundles such as rf-agentskills excluded), rf-core first, then the order of content_plugins().
# Plugins without a skills/ folder are left out. Paths are real directories of the installed
# package (wheel installs are unpacked), so they stay valid for the life of the process.

And in _assets/catalog.json, a top-level "format": 1 next to "plugins", so tools that read the file directly can check it too.

Compatibility promise (README, "Python API")

  • bundle.catalog(), bundle.skills_dirs() and bundle.CATALOG_FORMAT are public. Removing or renaming them, or removing a documented catalogue field, bumps CATALOG_FORMAT and is called out in the installer changelog.
  • New catalogue fields and new plugins aren't breaking changes.
  • The skill folder layout (<skills dir>/<skill-name>/SKILL.md plus references/) follows the Agent Skills format and is part of the contract.

Example: how rf-mcp would use it

# rf-mcp, src/robotmcp/skills/official.py (sketch)
from pathlib import Path

from packaging.specifiers import SpecifierSet

try:
    from robotframework_plugins import bundle
except ImportError:  # not installed: no official skills
    bundle = None


def official_skill_dirs() -> list[Path]:
    """Folders for rf-mcp's skill provider, below the project skills and --skills-dir."""
    if bundle is None or bundle.CATALOG_FORMAT != 1:
        return []
    return list(bundle.skills_dirs().values())


def plugins_for(project_packages: dict[str, str]) -> list[dict]:
    """The plugins whose libraries a project has installed, e.g. {"robotframework-browser": "20.1.0"}."""
    hits = []
    for plugin in bundle.catalog() if bundle else []:
        for lib in plugin.get("libraries", []):
            have = project_packages.get(lib["package"])
            if have is not None:
                hits.append({
                    "plugin": plugin["name"],
                    "library": lib["name"],
                    "installed": have,
                    "supported": have in SpecifierSet(lib.get("versions", "")),
                })
    return hits

For a project with robotframework-browser 20.1.0 installed, plugins_for(...) returns [{"plugin": "rf-web", "library": "Browser", "installed": "20.1.0", "supported": True}]. rf-mcp then attaches, for example, skill://rf-browser/references/locators.md to the result of a failed Click with a strict-mode error.

rf-mcp would declare robotframework-plugins>=2026.11,<2027 and check CATALOG_FORMAT at runtime.

Acceptance criteria

  • robotframework_plugins.bundle exists with CATALOG_FORMAT, catalog() and skills_dirs(), listed in its __all__.
  • _assets/catalog.json carries "format": 1.
  • Tests on a built wheel: skills_dirs() returns existing directories, each holding at least one */SKILL.md; catalog() names match the keys of skills_dirs() for the active content plugins; bundles are excluded.
  • The installer README has a short "Python API" section with the compatibility promise.
  • _assets.py keeps working for the installer's own adapters (the public module can wrap it).
  • Released to PyPI. robotframework-plugins is not on pypi.org yet, and rf-mcp can only depend on a published version.

Later, not part of this issue

A project where robotframework-plugins install already copied the skills into the agent's folders (for example .claude/skills/rf-browser/) would see the same skill from the agent and again from rf-mcp. A documented reader for the project install record (<project>/.rf-agentskills/installed.json), such as bundle.installed_skills(project) -> list[str], would let rf-mcp point to the copy that's already installed instead of serving a second one. That's a separate issue once this one has landed.

Activity

  1. manykarim commented on Nov 12, 2026

    @manykarim
    CollaboratorAuthor

    Implemented in #6.

    API (from robotframework_plugins import bundle), as proposed:

    Name Notes
    CATALOG_FORMAT 1; also written as "format": 1 at the top of _assets/catalog.json
    catalog() Active and deprecated plugins; a new list on each call. Each entry has name, version, status, tier, summary, libraries [{name, package, versions}], robotframework, dependencies, bundle, bundled
    skills_dirs() {plugin: Path} for active, non-bundle plugins with skills, rf-core first. Real folders for the life of the process, also when the package is imported from a zip

    Two differences from the proposal:

    • robotframework range: the build hook used to drop it from catalog.json; it's now in every entry.
    • New fetch(): bundle.fetch(ref="stable") returns a Snapshot with the same catalog() / skills_dirs() for a branch, tag or commit of this repository. It lets rf-mcp serve content newer than the installed package, with the bundled copy as the fallback.
      • Pinned: the ref is resolved to a commit, downloaded once from GitHub and cached by commit (~/.cache/robotframework-plugins/snapshots/<commit>/, or cache_dir=). A cached commit needs no network.
      • Checked: only plain files and folders, no paths outside the snapshot, valid catalog.yaml and plugin.json. Nothing in a snapshot is run.
      • Opt-in: any failure raises bundle.FetchError. Importing the module, catalog() and skills_dirs() never use the network.
      • Which ref: stable holds released plugins but only exists after the first plugin release; until then use main. main is merged and CI-checked but not released, so report Snapshot.commit when you serve it.

    For rf-mcp, the sketch from the issue works unchanged. With the fallback it looks like this:

    from robotframework_plugins import bundle
    
    def official_skill_dirs(use_main: bool = False) -> list[Path]:
        if bundle.CATALOG_FORMAT != 1:
            return []
        if use_main:
            try:
                return list(bundle.fetch("main").skills_dirs().values())
            except bundle.FetchError:
                pass  # offline or GitHub down: serve the bundled skills
        return list(bundle.skills_dirs().values())

    The compatibility promise is in the installer README under "Python API":

    • removing or renaming a public name, or removing or changing a listed field, bumps CATALOG_FORMAT;
    • new fields and plugins don't;
    • the skill folder layout is part of the contract.

    Status:

    Written with Claude Code (Anthropic) at the maintainer's request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions