On this page10 sections
Customization: the 5-layer pipeline itself is not replaceable — these layers explain how the framework turns your model + prompt config into a string. The deployer-facing API documented in this chapter is
PromptContentBuilder, which you call inside yourBasePrompt.promptContentto assemble the result. You consume the pipeline; you do not replace it.
Prompt derivation
Chapter 2’s derivation overview listed what the framework derives from your Model. This chapter walks the path the prompt subsystem actually takes — five layers between the static attributes block you wrote and the string an LLM ends up reading. Knowing the layers matters when you want to override one (a custom kind label, a section-level intro) without forking the rest.
A 5-layer architecture for generating prompt documentation from model and prompt configuration, eliminating manual duplication and ensuring consistency.
Table of Contents
- Architecture Overview
- Layer 1: Schema Derivation
- Layer 2: Grouping
- Layer 3: Section Documentation
- Layer 4: Assembly Pipeline
- Layer 5: Behavioral
- PromptContentBuilder API
- Migration Guide
- Content Categories
Architecture Overview
Data flows bottom-up: Model config → field definitions → grouped sections → assembled documentation → behavioral instructions.
Layer 1: Schema Derivation
File: src/mcp/model-layer/schema-derivation.ts (public import: @mcp-rune/mcp-rune/prompts)
Generates fieldDefinitions from model’s attributes. This is the foundation — all field metadata comes from the model.
import { derivePromptSchema } from '@mcp-rune/mcp-rune/prompts'
import { Activity } from '../models/index.js'
static {
const schema = derivePromptSchema(Activity, {
fieldGroups: this.fieldGroups,
fieldOverrides: {
// Override/extend fields from model
theme_id: { required: true }
},
promptFields: {
// Prompt-only fields not in model
book_ids: { name: 'book_ids', type: 'array', required: false }
}
})
this.fieldGroups = schema.fieldGroups
this.fieldDefinitions = schema.fieldDefinitions
}import { derivePromptSchema } from '@mcp-rune/mcp-rune/prompts'
import { Activity } from '../models/index.js'
static {
const schema = derivePromptSchema(Activity, {
fieldGroups: this.fieldGroups,
fieldOverrides: {
// Override/extend fields from model
theme_id: { required: true }
},
promptFields: {
// Prompt-only fields not in model
book_ids: { name: 'book_ids', type: 'array', required: false }
}
})
this.fieldGroups = schema.fieldGroups
this.fieldDefinitions = schema.fieldDefinitions
}Key principle: The model’s attributes is the single source of truth. derivePromptSchema() reads type, description, examples, enumValues, enumDescriptions, default, validation, conditional, and required from the model and assembles them into fieldDefinitions.
Layer 2: Grouping
File: Prompt class static properties
Detailed reference: Sections & Field Groups guide. This section presents grouping as a layer of the derivation pipeline; the linked guide is the canonical reference for the two structures themselves.
Two complementary structures organize fields:
Sections (User-facing)
static sections = {
classification: {
title: 'Classification',
description: 'Theme and category',
required: true,
groups: ['classification'],
content: {
intro: 'Classification determines how activities are organized.',
notes: ['Use find_records to look up themes']
}
}
}static sections = {
classification: {
title: 'Classification',
description: 'Theme and category',
required: true,
groups: ['classification'],
content: {
intro: 'Classification determines how activities are organized.',
notes: ['Use find_records to look up themes']
}
}
}FieldGroups (Validation)
static fieldGroups = {
classification: {
fields: ['theme_id', 'category_id'],
context: 'Classification',
required: true
}
}static fieldGroups = {
classification: {
fields: ['theme_id', 'category_id'],
context: 'Classification',
required: true
}
}Section content enrichment: The content.intro and content.notes properties are automatically included in generated section documentation (Layer 3). Use these to add domain-specific context without writing custom section generator methods:
content.intro(string) — Rendered before the field table. Supports full markdown.content.notes(string[]) — Rendered as a bullet list after the field table and enum tables.askPrompt(string) — Custom “Ask the user: …” prompt at the end of the section.
Key principle: Prefer content.intro/content.notes over customSections overrides in allSections(). This keeps domain content in configuration while the framework auto-generates field tables and enum tables alongside it.
Layer 3: Section Documentation
Files: src/mcp/prompts/generators/, src/mcp/prompts/prompt-content-builder.ts
Generates per-section documentation from config. Includes:
- Field tables (name, required, description)
- Enum value tables (from
enumDescriptionsin model config) - Section intro text (from
content.intro) - Section notes (from
content.notes) - “Ask the user” prompts
- Validation reminders
Generator functions
Rendering lives in pure functions under src/mcp/prompts/generators/, reached only through PromptContentBuilder — never import a generator directly:
| Function | File | Output |
|---|---|---|
generateSection / generateAllSections | generators/section-generator.ts | Complete section doc(s) with field table, enum tables, ask prompt |
renderEnumTable / renderEnumTables | generators/helpers.ts | Markdown table(s) of enum values with descriptions |
generateAttributeReference | generators/attribute-reference-generator.ts | Full attribute reference table |
generateSummary | generators/summary-generator.ts | Standard summary/confirmation section |
Enum Tables
When a model field has enumDescriptions, enum tables are automatically generated:
// In model:
static attributes = {
status: {
type: 'enum',
enumValues: ['planned', 'active', 'paused', 'completed', 'archived'],
default: 'planned',
enumDescriptions: {
planned: 'Not yet started',
active: 'Currently in progress',
paused: 'Temporarily on hold',
completed: 'Finished',
archived: 'No longer relevant'
}
}
}// In model:
static attributes = {
status: {
type: 'enum',
enumValues: ['planned', 'active', 'paused', 'completed', 'archived'],
default: 'planned',
enumDescriptions: {
planned: 'Not yet started',
active: 'Currently in progress',
paused: 'Temporarily on hold',
completed: 'Finished',
archived: 'No longer relevant'
}
}
}Generated output:
**`status` values:**
| Value | Description |
| ------------- | ----------------------------- |
| `"planned"` | Not yet started **(default)** |
| `"active"` | Currently in progress |
| `"paused"` | Temporarily on hold |
| `"completed"` | Finished |
| `"archived"` | No longer relevant |
Layer 4: Assembly Pipeline
File: src/mcp/prompts/prompt-content-builder.ts
The PromptContentBuilder builder composes all layers into final promptContent.
get promptContent() {
return PromptContentBuilder.for(ActivityPrompt, 'activity')
.add(`# Activity Creation Guide
## What is an Activity?
Custom intro text...`)
.standard() // flowDiagram → guidance → allSections → summary
.toolUsage() // Auto-generated tool docs
.attributeReference() // Layer 3: attribute reference table
.build() // Join with '\n\n---\n\n'
}get promptContent() {
return PromptContentBuilder.for(ActivityPrompt, 'activity')
.add(`# Activity Creation Guide
## What is an Activity?
Custom intro text...`)
.standard() // flowDiagram → guidance → allSections → summary
.toolUsage() // Auto-generated tool docs
.attributeReference() // Layer 3: attribute reference table
.build() // Join with '\n\n---\n\n'
}Parts are joined with \n\n---\n\n (horizontal rules) by default.
Layer 5: Behavioral
File: src/mcp/prompts/generators/guidance-generator.ts — generateGuidance()
Only applies to stateful prompts. Generates:
- Mode selection (guided vs quick)
- Turn-taking enforcement rules
- Section-by-section validation requirements
- Forbidden/correct behavior patterns
Accessed via .guidance() in the builder, which .standard() already includes.
PromptContentBuilder API
Factory
PromptContentBuilder.for(PromptClass, 'model_name')PromptContentBuilder.for(PromptClass, 'model_name')Builder Methods
| Method | Description | Use With |
|---|---|---|
.add(content) | Add custom markdown content | All strategies |
.standard(options?) | Canonical: flowDiagram → guidance → beforeSections → allSections → summary | All |
.flowDiagram() | Step-by-step roadmap from sections/fieldGroups config | All strategies |
.guidance() | Stateful guidance instructions (Layer 5) | Stateful only |
.section(groupName, num, options) | Single section documentation | Stateful |
.allSections({ skip, customSections }) | All sections from config | Stateful |
.summary() | Standard summary/confirmation template | Stateful |
.toolUsage(overrides?) | Auto-generated tool usage docs from static toolUsage config | All strategies |
.attributeReference() | Auto-generated attribute table | All strategies |
.build(separator) | Join parts (default: \n\n---\n\n) | All |
.allSections() Options
.allSections({
skip: ['content'], // Skip sections handled by custom .add() calls
customSections: {
// Override specific sections with custom generators
resources: (sectionNum) => `## SECTION ${sectionNum}: Resources\n...custom content...`
}
}).allSections({
skip: ['content'], // Skip sections handled by custom .add() calls
customSections: {
// Override specific sections with custom generators
resources: (sectionNum) => `## SECTION ${sectionNum}: Resources\n...custom content...`
}
})Migration Guide
Before (manual documentation)
get promptContent() {
return `
# My Guide
...intro...
| Field | Required | Description |
|-------|----------|-------------|
| name | Yes | The name | ← Hardcoded, will drift from model
| type | No | The type |
## Summary
...manual summary...
## Attribute Reference
${this.generateAttributeReference()} ← Custom method per prompt
`
}get promptContent() {
return `
# My Guide
...intro...
| Field | Required | Description |
|-------|----------|-------------|
| name | Yes | The name | ← Hardcoded, will drift from model
| type | No | The type |
## Summary
...manual summary...
## Attribute Reference
${this.generateAttributeReference()} ← Custom method per prompt
`
}After (framework)
import { PromptContentBuilder } from '@mcp-rune/mcp-rune/prompts'
get promptContent() {
return PromptContentBuilder.for(MyPrompt, 'my_model')
.add(`# My Guide\n\n...intro...`)
.standard()
.toolUsage()
.attributeReference() // One line replaces 20+ lines
.build()
}import { PromptContentBuilder } from '@mcp-rune/mcp-rune/prompts'
get promptContent() {
return PromptContentBuilder.for(MyPrompt, 'my_model')
.add(`# My Guide\n\n...intro...`)
.standard()
.toolUsage()
.attributeReference() // One line replaces 20+ lines
.build()
}Migration Steps
- Add
import { PromptContentBuilder } from '@mcp-rune/mcp-rune/prompts' - Replace
promptContentgetter with builder pipeline - Remove
generateAttributeReference()→ replaced by.attributeReference() - Remove
generateSummarySection()→ replaced by.summary()(stateful) - Keep domain-specific methods (tool usage, custom sections) as
.add()calls - Update tests if they check for specific format strings
Content Categories
When migrating, classify each piece of content:
| Category | Description | Action |
|---|---|---|
| A: Auto-generatable | Summary templates, attribute references | Replace with .summary(), .attributeReference() |
| B: Config-generatable | Section documentation, enum tables | Use .allSections() or .section(), enrich content.notes |
| C: Custom | Intro text, tool usage, domain-specific logic | Keep as .add() calls |
Rule of thumb: If the content depends only on fieldDefinitions, fieldGroups, or sections, it’s auto-generatable. If it requires runtime state or domain knowledge, keep it as .add().