r mcp-rune 0.1.0
SECTION II · GUIDE 04 OF 19
Reading
14 min
Topic
core · dsl
Spec
v0.1.0-alpha
Source
prompt-creation-guide.md

Prompt Creation Guide

This document provides guidelines for creating MCP prompts in this codebase.

Table of Contents

Overview

Prompts guide LLM interactions for creating/updating models. They define:

  • Sections for user-facing workflow structure
  • Field groups for validation and technical organization
  • Field definitions with validation rules
  • Prompt content for domain-specific documentation
  • MCP arguments for discoverability

Prompt Strategies

StrategyUse CaseFieldsValidation
statelessSimple forms< 10 fieldsNone before submit
hybridMedium forms10-20 fieldsFull form validation
statefulComplex forms20+ fieldsSection-by-section

Sections & field groups

Sections define the user-facing workflow; field groups carry validation. The two work together — one section can contain one or more field groups, and the framework auto-generates field tables, enum tables, and flow diagrams from both.

For the full reference — section content enrichment, per-group content for multi-group sections, helper methods, and flow-diagram generation — see the Sections & Field Groups guide.

In short:

  1. Separation of concerns — display structure (sections) vs validation structure (fieldGroups)
  2. Scalability — add new fieldGroups to existing sections without changing the user-facing workflow
  3. Flexibility — multiple fieldGroups can be grouped under one section
  4. Single source of truth — section titles and descriptions defined once

Schema Derivation

Field definitions are derived from model classes via derivePromptSchema(). This eliminates duplication between models and prompts.

How It Works

import { derivePromptSchema } from '#src/mcp/prompts/schema-derivation.js'
import { Activity } from '../models/index.js'

export class ActivityPrompt extends BasePrompt {
  static fieldGroups = {
    basics: {
      fields: ['title', 'description'],
      context: 'Basic Information',
      required: true
    }
  }

  // Schema derivation: generates fieldDefinitions FROM model's attributes
  static {
    const schema = derivePromptSchema(Activity, {
      fieldGroups: this.fieldGroups,
      fieldOverrides: {
        theme_id: { required: true }
      },
      promptFields: {
        book_ids: { name: 'book_ids', type: 'array', required: false }
      }
    })

    this.fieldGroups = schema.fieldGroups
    this.fieldDefinitions = schema.fieldDefinitions
  }
}

Key Principles

  1. Model is source of truth: attributes contains ALL field metadata
  2. Prompt groups fields: fieldGroups specifies which fields belong together
  3. Schema derivation bridges them: derivePromptSchema() generates fieldDefinitions from model config
  4. Never hardcode field tables: Use generated documentation from fieldDefinitions

PromptContentGenerator

The PromptContentGenerator is a fluent builder for assembling prompt content from configuration. It implements Layers 3-4 of the derivation framework.

Usage

import { PromptContentGenerator } from '#src/mcp/prompts/prompt-content-generator.js'

get promptContent() {
  return PromptContentGenerator.for(ActivityPrompt, 'activity')
    .add(`# Activity Creation Guide

## What is an Activity?
...custom intro text...`)
    .standard()           // flowDiagram → guidance → allSections → summary
    .add(this.generateToolUsageSection())  // Custom tool usage
    .attributeReference() // Auto-generated attribute reference table
    .build()
}

Builder Methods

MethodDescriptionStrategy
.add(content)Add custom markdown contentAll
.standard(options?)Canonical: flowDiagram → guidance → beforeSections → allSections → summaryAll
.flowDiagram()Add step-by-step roadmapAll
.guidance()Add stateful guidance instructionsStateful only
.section(groupName, num)Add single section documentationStateful
.allSections({ skip, customSections })Add all section docsStateful
.summary()Add standard summary templateStateful
.attributeReference()Add auto-generated attribute tableAll
.build(separator)Join all parts (default: \n\n---\n\n)All

Atomic Helpers on BasePrompt

The builder delegates to these static methods on BasePrompt:

MethodDescription
generateEnumTable(fieldName)Enum value table with descriptions
generateAttributeReferenceFromConfig()Full attribute reference table
generateSummaryTemplate(modelName)Standard summary/confirmation section

Strategy Patterns

Standard (all strategies — preferred):

PromptContentGenerator.for(ActivityPrompt, 'activity')
  .add(intro)
  .standard() // Enforces canonical ordering
  .add(toolUsage)
  .attributeReference()
  .build()

With custom sections (skip pattern):

PromptContentGenerator.for(MyPrompt, 'model')
  .add(intro)
  .standard({
    beforeSections: [customSection], // Inserted before allSections
    skip: ['content'] // Skipped in allSections
  })
  .add(toolUsage)
  .attributeReference()
  .build()

Stateful prompts

For complex (20+ field) forms, mcp-rune ships a stateful strategy that walks the agent through one section at a time and validates as it goes. The strategy supports two interaction modes — guided (step-by-step for humans) and quick (minimal questions for agentic flows) — and exposes section progress through the StatefulStrategy API.

For mode configuration, the prompt class structure, the BasePrompt helpers, the validation flow, and the StatefulStrategy.getSections() / getProgress() reference, see the Stateful Strategies guide.

Stateless Prompts

For simple models (< 10 fields), use stateless strategy:

export class ThemePrompt extends BasePrompt {
  static strategy = 'stateless'

  static fieldGroups = {
    theme_identity: {
      fields: ['name', 'slug'],
      required: true
    }
  }

  // No mode argument needed - stateless prompts don't have sections
  static arguments = [{ name: 'name', description: 'Theme name', required: false }]
}

Stateless prompts:

  • Collect all fields at once
  • No per-section validation
  • No mode selection (no sections to walk through)

Registry Configuration

Register prompts in prompts/registry.js:

const PROMPT_CLASSES = {
  create_activity: {
    promptClass: ActivityPrompt,
    model: 'activity',
    toolDocDescription: 'For tracking learning activities with timing and resources',
    required: true,
    recommendedForBulk: false
  }
}
PropertyDescription
promptClassReference to prompt class
modelModel name for validation/creation
toolDocDescriptionShown in tool documentation
requiredIf true, create_model/update_model blocked without get_prompt_guide
recommendedForBulkIf true, suggest prompt for bulk operations

Testing Prompts

Test files should verify sections architecture:

describe('ActivityPrompt', () => {
  describe('static properties', () => {
    it('should have strategy of stateful', () => {
      expect(ActivityPrompt.strategy).toBe('stateful')
    })

    it('should have mode in arguments', () => {
      const argNames = ActivityPrompt.arguments.map((a) => a.name)
      expect(argNames).toContain('mode')
    })
  })

  describe('sections architecture', () => {
    it('should have sections defined', () => {
      expect(ActivityPrompt.sections).toBeDefined()
      expect(Object.keys(ActivityPrompt.sections).length).toBeGreaterThan(0)
    })

    it('each section has required properties', () => {
      for (const [name, section] of Object.entries(ActivityPrompt.sections)) {
        expect(section.title).toBeDefined()
        expect(section.description).toBeDefined()
        expect(typeof section.required).toBe('boolean')
        expect(Array.isArray(section.groups)).toBe(true)
      }
    })

    it('all groups in sections exist in fieldGroups', () => {
      const fieldGroupNames = Object.keys(ActivityPrompt.fieldGroups)
      for (const [, section] of Object.entries(ActivityPrompt.sections)) {
        for (const groupName of section.groups) {
          expect(fieldGroupNames).toContain(groupName)
        }
      }
    })
  })
})

File-Based Snapshot Tests

Use toMatchFileSnapshot() to capture the complete rendered promptContent as individual .prompt.md files:

import { join } from 'node:path'

const SNAP_DIR = join(import.meta.dirname, '__file_snapshots__')
const snap = (name) => join(SNAP_DIR, `${name}.prompt.md`)

describe('Prompt Snapshots', () => {
  it('ActivityPrompt renders full output', async () => {
    const instance = new ActivityPrompt({})
    await expect(instance.promptContent).toMatchFileSnapshot(snap('activity-prompt'))
  })

  it('BookPrompt renders full output', async () => {
    const instance = new BookPrompt({})
    await expect(instance.promptContent).toMatchFileSnapshot(snap('book-prompt'))
  })
})

Key rules:

  • All assertions must use awaittoMatchFileSnapshot is async
  • Naming convention: {prompt-name}--{variant}.prompt.md
  • Update snapshots after intentional changes: npx vitest run --update

Checklist for New Prompts

All Prompts

  • Choose strategy: stateless, hybrid, or stateful
  • Define sections with user-facing structure (title, description, required, groups)
  • Define fieldGroups with validation structure (fields, required, conditional)
  • Use derivePromptSchema() to generate fieldDefinitions from model
  • Use PromptContentGenerator builder in promptContent getter
  • Use .standard() for canonical pipeline ordering
  • Use .attributeReference() instead of manual attribute tables
  • Register in prompts/registry.js
  • Add unit tests
  • Add file-based snapshot test(s) in prompt-snapshots.spec.js

Additional for Stateful Prompts

  • Add mode to static arguments
  • Use .standard({ beforeSections, skip }) for custom section handling
  • Enrich sections with content.intro and content.notes for domain-specific context
  • Ensure all groups in sections exist in fieldGroups
  • Add tests for sections architecture