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.
Two steps. Total wiring: one config line.
install.packages("fortunes")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"]}
]
}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:
fortune(showMatches = TRUE) to find anything about
NULL, then summarize the consensus.”The R community has 20,000 CRAN packages but not all of them work cleanly as agent skills. The shape that fits:
cat()s status updates or paints with
crayon:: pollutes the tool result.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.
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.
Rscript --vanilla -e 'saber::pkg_exports("fortunes")' and
Rscript --vanilla -e 'saber::pkg_help("fortune", "fortunes")'..R file in
<project>/.corteza/skills/ calling
register_skill_from_fn("name", my_fn). Loaded on every
session start, no installation required.