Package as Skill

corteza logo

Package as Skill

An R package that documents its functions properly is already what MCP is trying to wire together: tools (functions), descriptions (.Rd files), and an invocation mechanism (library()). corteza walks the .Rd tree at session start and turns the exports into JSON-Schema tool definitions — same shape the Anthropic / OpenAI / Moonshot APIs expect. No server, no schema by hand, no protocol.

This vignette walks the setup with fortunes, quotes from the R-help archives. It returns a structured S3 object, and the demo is more fun than configuring a JSON parser.

Setup

Two steps. Total wiring: one config line.

1. Install the package

install.packages("fortunes")

2. Register it as a skill

Add a skill_packages entry to corteza’s config. Either project-local at <cwd>/.corteza/config.json or global at tools::R_user_dir("corteza", "config")/config.json:

{
  "skill_packages": ["fortunes"]
}

That’s it. The string form registers every export. For larger packages, the object form picks specific functions:

{
  "skill_packages": [
    {"package": "fortunes", "functions": ["fortune"]}
  ]
}

Use

corteza::chat()

The startup banner shows the tool count climbing — corteza::chat() reports 30 tools instead of 29. Then ask the agent to use it:

> Find a fortune by Brian Ripley about types or coercion.

You’ll see the progress hint fire as the agent invokes the tool:

  [fortunes::fortune] author=Brian Ripley (8 lines)

The fortune object comes back with quote, author, context, source, and date fields, and the agent paraphrases or quotes from there. Try variations:

  • “Pick a random fortune and tell me which R personality you’d most love to argue with about it.”
  • “Use fortune(showMatches = TRUE) to find anything about NULL, then summarize the consensus.”
  • “Show me three fortunes about S4 classes and rank them by snark.”

What makes a package a good fit

The R community has 20,000 CRAN packages but not all of them work cleanly as agent skills. The shape that fits:

  • Returns structured data (data.frames, lists, S3 objects). The LLM consumes the result programmatically.
  • No printing to stderr on the happy path. Anything that cat()s status updates or paints with crayon:: pollutes the tool result.
  • No oops() or stop() for non-error cases. If the function calls stop() because you’re not in the right directory, it’s designed for a human at the console, not a tool harness.
  • .Rd parameters that match formals(). CRAN already enforces this with R CMD check, so any current CRAN package qualifies.

Counter-example: gitr wraps system2("git", ...) cleanly enough but cat()s colored output and calls oops() when not in a git repo. Wrapping it as a skill needed utils::capture.output() and grew the code instead of shrinking it. Built for humans, not for plumbing.

The test: would you call this function from another function and trust the return value? If yes, it’s a candidate.

Why no MCP server

A live MCP server ships every tool’s JSON schema into the system prompt at connect time. Twenty tools at ~400 tokens each is 8,000 tokens of startup overhead before the agent has done anything. The CLI / package-as-skill path pays roughly zero startup tax (corteza already has bash, run_r, read_file baked in), and lazy lookup via saber::pkg_help() costs ~200 tokens for a tool the agent actually needs.

Same tool surface, different cost curve. corteza’s MCP server (corteza::serve()) still exists for clients that need it (Claude Code, Codex, etc), but it’s no longer the only path — the same skill registry feeds both.

Where to go next

  • The derivation pipeline is summarized in the package vignette and package source; the earlier external blog post is no longer linked because its URL no longer resolves reliably.
  • Discover what’s in a package the same way the agent does: Rscript --vanilla -e 'saber::pkg_exports("fortunes")' and Rscript --vanilla -e 'saber::pkg_help("fortune", "fortunes")'.
  • For your own functions, drop an .R file in <project>/.corteza/skills/ calling register_skill_from_fn("name", my_fn). Loaded on every session start, no installation required.