Repository navigation
Description
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:
- The catalogue: plugin names, versions,
libraries[{name, package, versions}]and therobotframeworkrange, to match a project's installed packages against the plugins. - The skill folders: the
skills/folder of each bundled plugin as a real filesystem path. rf-mcp serves skills from directories (FastMCP'sSkillsDirectoryProvider), with a project's own skills and a company's--skills-diroverriding them by name. - A stable contract: a version range of
robotframework-pluginsthat 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()andbundle.CATALOG_FORMATare public. Removing or renaming them, or removing a documented catalogue field, bumpsCATALOG_FORMATand 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.mdplusreferences/) 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 hitsFor 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.bundleexists withCATALOG_FORMAT,catalog()andskills_dirs(), listed in its__all__. -
_assets/catalog.jsoncarries"format": 1. - Tests on a built wheel:
skills_dirs()returns existing directories, each holding at least one*/SKILL.md;catalog()names match the keys ofskills_dirs()for the active content plugins; bundles are excluded. - The installer README has a short "Python API" section with the compatibility promise.
-
_assets.pykeeps working for the installer's own adapters (the public module can wrap it). - Released to PyPI.
robotframework-pluginsis 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
manykarim commentedon Nov 12, 2026
Implemented in #6.
API (
from robotframework_plugins import bundle), as proposed:CATALOG_FORMAT1; also written as"format": 1at the top of_assets/catalog.jsoncatalog()name,version,status,tier,summary,libraries[{name, package, versions}],robotframework,dependencies,bundle,bundledskills_dirs(){plugin: Path}for active, non-bundle plugins with skills,rf-corefirst. Real folders for the life of the process, also when the package is imported from a zipTwo differences from the proposal:
robotframeworkrange: the build hook used to drop it fromcatalog.json; it's now in every entry.fetch():bundle.fetch(ref="stable")returns aSnapshotwith the samecatalog()/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.~/.cache/robotframework-plugins/snapshots/<commit>/, orcache_dir=). A cached commit needs no network.catalog.yamlandplugin.json. Nothing in a snapshot is run.bundle.FetchError. Importing the module,catalog()andskills_dirs()never use the network.stableholds released plugins but only exists after the first plugin release; until then usemain.mainis merged and CI-checked but not released, so reportSnapshot.commitwhen you serve it.For rf-mcp, the sketch from the issue works unchanged. With the fallback it looks like this:
The compatibility promise is in the installer README under "Python API":
CATALOG_FORMAT;Status:
format, README, wheel test (a built wheel installed into a fresh venv)fetch()tested against a local fake GitHub, and once against GitHub (main→ 3756236)2026.11.0to PyPI (tagrobotframework-plugins-v2026.11.0, this month)Written with Claude Code (Anthropic) at the maintainer's request.