mcp-rune 0.107.0
Be star #1 Get started
SECTION IV · GUIDE 16 OF 49
Reading
6 min
Topic
tools
Spec
v0.107.0
Source
04-tools/polymorphic-tools.md
On this page6 sections

Polymorphic tools

The previous chapter introduced the three layers every tool sees at runtime. This chapter looks at the tools the framework actually ships — and explains why a small, fixed set of nine tools is enough to serve every model you’ll ever define.

The polymorphic promise: the LLM’s tool list does not grow with your domain. Add a tenth model to your server, and you do not add a tenth tool — the same 9 tools include the new model on next boot.

The shape

                    Polymorphic surface (does not grow with N models)

   ┌─ CRUD ─────────────┐  ┌─ Form strategy ────┐  ┌─ Optional (extensions) ─┐
   │ list_models        │  │ get_prompt_guide   │  │ search_records          │
   │ find_records       │  │ validate_form      │  │ get_filters_guide       │
   │ create_model       │  │ get_form_summary   │  └─────────────────────────┘
   │ update_model       │  └────────────────────┘
   │ delete_model       │
   │ bulk_action_models │
   └────────────────────┘

   ┌──────────────────────────────────────────────────────────────────────────┐
   │                       Your models (any number)                            │
   │     Book   Author   Genre   Project   Task   Tag   Comment   …            │
   └──────────────────────────────────────────────────────────────────────────┘

Every tool takes a model: argument and uses the registry to look up the matching ModelClass. From the LLM’s point of view it’s “call find_records with model: 'book'”, not “call find_books”. When you add a model, the polymorphic dispatch picks it up automatically.

The bundled nine

CRUD — the six tools of DATA_TOOL_CLASSES

ToolWhat it doesReaches
list_modelsLists every registered model + its schema summarythis.models registry only — no I/O
find_recordsPaginated read for a model, with filters and includesthis.dataLayer.find / .list
create_modelValidated write — runs schema + association passesthis.dataLayer.create
update_modelSame as create, but updates by idthis.dataLayer.update
delete_modelDelete by idthis.dataLayer.delete
bulk_action_modelsBatch create / update / delete in one callthis.dataLayer.dispatch

All six are wired in via DATA_TOOL_CLASSES from @mcp-rune/mcp-rune/tools. In your config.ts:

File: bookshelf/config.ts

examples/polymorphic-tools-01.ts
import {
  DATA_TOOL_CLASSES,
  FORM_STRATEGY_TOOL_CLASSES,
  ToolRegistry
} from '@mcp-rune/mcp-rune/tools'

new ToolRegistry({
  toolClasses: { ...DATA_TOOL_CLASSES, ...FORM_STRATEGY_TOOL_CLASSES },
  models: { book: Book, author: Author, genre: Genre }
  // …
})
import {
  DATA_TOOL_CLASSES,
  FORM_STRATEGY_TOOL_CLASSES,
  ToolRegistry
} from '@mcp-rune/mcp-rune/tools'
new ToolRegistry({
  toolClasses: { ...DATA_TOOL_CLASSES, ...FORM_STRATEGY_TOOL_CLASSES },
  models: { book: Book, author: Author, genre: Genre }
  // …
})

There is no per-model wiring. ToolRegistry reads the models: map and the tools take it from there.

Form-strategy — three tools over the prompt registry

ToolWhat it does
get_prompt_guideReturns the prompt content (chapter 3) for one model — the LLM’s form filler
validate_formRuns the kind validators on a partial payload — no backend call
get_form_summaryReturns the “what would I be submitting” human-readable summary

These three tools don’t take an authenticated dataLayervalidate_form deliberately can’t write, because its purpose is to give the LLM cheap feedback before it commits. They read kind descriptors from src/mcp/models/kinds/ (shared vocabulary — ADR 0009) and consume PromptRegistry for the guide text.

Two optional search tools

ToolWhen you get it
search_recordsWhen the search ApiExtension is registered
get_filters_guideSame

Both ship in @mcp-rune/mcp-rune/api-extensions/search. They’re not part of DATA_TOOL_CLASSES because not every backend has a search endpoint — opting in is a single line in config.ts. See API extensions.

Why the surface stops at nine

The pattern the framework follows: a polymorphic tool exists when its operation is uniform across models. Six CRUD verbs are uniform — every model has find, list, create, update, delete, and a batched form of each. Three form-strategy operations are uniform — every model has a derivable prompt guide, a validatable form, and a summarizable payload.

Operations that are not uniform across models — cancel a subscription, retry a payment, recompute a leaderboard — are intentionally not polymorphic. They are bespoke BaseTool subclasses you write per operation. The next chapter, Tool creation, covers that path.

How the LLM discovers the surface

list_models is the entry point. The LLM calls it with {} and gets back every registered model plus a short attribute summary — that’s enough to pick the right model: value for any subsequent CRUD call. From there:

list_models()             → "what exists?"
get_prompt_guide(book)    → "how do I fill the form?"
validate_form(book, …)    → "is this payload OK?"
create_model(book, …)     → "submit it"
find_records(book, …)     → "see what landed"

That’s the loop the quickstart walked you through with Book. The same loop works for Tag, Task, Project, or anything else in your models: map, with zero per-model code on your side.

See it running

The bookshelf example exercises all nine tools end-to-end against three seed books. The bookshelf/TUTORIAL.md walks through each call in order — it’s the runnable companion to this chapter.

What’s next

When a bespoke verb doesn’t fit the polymorphic shape, you reach for Tool creation and write a BaseTool subclass. The next chapter covers the contract: inputSchema (a Zod raw shape), the static requires* flags, execute, the interceptor pipeline, and the moments you’d customize each.