mcp-rune 0.107.0
Be star #1 Get started
SECTION VIII · GUIDE 33 OF 49
Reading
24 min
Topic
domain
Spec
v0.107.0
Source
08-domain-knowledge/domain-knowledge.md
On this page11 sections

Domain knowledge framework

This guide explains how to extend the domain intelligence layer with new concepts, business rules, and workflows. The framework is declarative: all domain components are data structures, not procedural code.

Table of Contents

  1. Architecture Overview
  2. When to Use Each Component
  3. DomainConcept — Cross-Entity Knowledge
  4. BusinessRule — Declarative Constraints
  5. WorkflowDefinition — Multi-Step Guides
  6. Wiring It Up — The Domain Registry
  7. How Domain Tools Consume the Registry
  8. Step-by-Step: Adding Domain Intelligence to a New Server
  9. Semantic Search
  10. Testing

Architecture Overview

lib/mcp/domain/ # Framework classes (shared) ├── knowledge.js # DomainConcept + DomainKnowledge ├── business-rules.js # BusinessRule + RuleSet ├── workflows.js # WorkflowStep + WorkflowDefinition + WorkflowRegistry └── registry.js # DomainRegistry (aggregates all of the above) lib/mcp/tools/domain/ # Domain tools (shared) ├── base-domain-tool.js # BaseDomainTool (DOMAIN category, no auth) ├── get-domain-context-tool.js # Retrieves composed context for a model/concept ├── check-business-rules-tool.js # Validates data against business rules ├── suggest-workflow-tool.js # Returns a workflow roadmap + first step └── get-workflow-step-tool.js # Returns detail for a specific workflow step src/<server>/domain/ # Server-specific domain data ├── registry.js # Factory: createXxxDomainRegistry() ├── knowledge/ │ └── concepts.js # DomainConcept instances ├── rules/ │ ├── <domain>-rules.js # BusinessRule instances │ └── mutability-rules.js # Auto-generated from model metadata └── workflows/ └── <workflow-category>.js # WorkflowDefinition instances

Data flow:

  1. Server code defines DomainModule objects — one per domain area (catchup, deals, …)
  2. Modules are passed to InMemoryDomainAdapter, which constructs the value-object instances
  3. The adapter is wrapped in DomainRegistry and dependency-injected into the tool registry
  4. Four domain tools (get_domain_context, check_business_rules, suggest_workflow, get_workflow_step) consume the registry and expose it to users

DomainModule — the bundling type

DomainModule (from domain-definitions.ts) groups concepts, rules, and workflows for one domain area instead of maintaining separate arrays per resource type:

examples/domain-knowledge-01.ts
import type { DomainModule } from '@mcp-rune/mcp-rune/domain'
import { DomainConcept, BusinessRule, WorkflowDefinition } from '@mcp-rune/mcp-rune/domain'

export const taskModule: DomainModule = {
  concepts: [new DomainConcept({ name: 'task_lifecycle', ... })],
  rules:    [new BusinessRule({ name: 'task_requires_due_date', ... })],
  workflows: [new WorkflowDefinition({ name: 'setup_project', ... })]
}
import { DomainConcept, BusinessRule, WorkflowDefinition } from '@mcp-rune/mcp-rune/domain';
export const taskModule = {
    concepts: [new DomainConcept({ name: 'task_lifecycle', ... })],
    rules: [new BusinessRule({ name: 'task_requires_due_date', ... })],
    workflows: [new WorkflowDefinition({ name: 'setup_project', ... })]
};

Multiple modules are merged by the adapter:

examples/domain-knowledge-02.ts
new InMemoryDomainAdapter([taskModule, projectModule, bookModule])
new InMemoryDomainAdapter([taskModule, projectModule, bookModule])

DomainAdapter — the storage backend

DomainRegistry accepts any DomainAdapter implementation. The default is InMemoryDomainAdapter — items live in process memory, search defaults to substring matching with MiniLM embeddings available opt-in via initSearch('embedding'). Remote adapters (PGVector, Qdrant) are not yet shipped; the interface supports them once a seeding/sync mechanism is designed.


When to Use Each Component

I want to…Use
Explain how entities relate to each otherDomainConcept
Document inheritance, terminology, or processesDomainConcept
Validate constraints between entitiesBusinessRule
Catch field-level errors before API callsBusinessRule
Guide users through a multi-step processWorkflowDefinition
Create a demo or troubleshooting scriptWorkflowDefinition

Key distinction: Concept vs Rule vs Workflow

  • Concept = knowledge about what exists and how things relate (“projects contain tasks, which inherit priority and due date”)
  • Rule = a testable constraint that can pass or fail (“in-progress tasks must have a due_date”)
  • Workflow = an ordered sequence of steps to accomplish a goal (“set up a project with tasks in 5 steps”)

If you find yourself writing a concept with an evaluate function, make it a rule. If you find yourself writing a rule with 10 steps, make it a workflow.


DomainConcept — Cross-Entity Knowledge

Source: src/mcp/domain/knowledge.ts

A DomainConcept captures knowledge that spans multiple models and cannot be expressed in any single model’s static attributes. Field-level knowledge (enum descriptions, conditionals, examples) belongs in models — concepts add cross-entity relationships on top.

Constructor

examples/domain-knowledge-guide-01.ts
import { DomainConcept } from '@mcp-rune/mcp-rune/domain'

new DomainConcept({
  name: 'project_task_hierarchy',              // Unique identifier (snake_case)
  title: 'Project → Task → Tag',               // Human-readable title
  description: 'Projects contain tasks...',   // 1-2 sentence explanation
  models: ['project', 'task', 'tag'],          // Models this concept spans
  tags: ['hierarchy', 'structure'],            // For filtering and search
  details: { ... }                             // Structured additional context
})
import { DomainConcept } from '@mcp-rune/mcp-rune/domain'

new DomainConcept({
  name: 'project_task_hierarchy',              // Unique identifier (snake_case)
  title: 'Project → Task → Tag',               // Human-readable title
  description: 'Projects contain tasks...',   // 1-2 sentence explanation
  models: ['project', 'task', 'tag'],          // Models this concept spans
  tags: ['hierarchy', 'structure'],            // For filtering and search
  details: { ... }                             // Structured additional context
})

The details Object

The details object is freeform by design — the get_domain_context tool renders it as structured content to the LLM, so any keys work. However, the formatting layer recognizes certain conventional keys and renders them specially:

KeyTypeRenderingPurpose
inheritance{ from, to, fields }**Inheritance:** project → task (fields: ...)Describes field inheritance between entities
processstring**Process:** Create project → add tasks → ...Step-by-step procedure
tipsstring[]Bulleted list under **Tips:**Actionable guidance

Other keys are passed through as-is in the JSON context. Common conventions:

KeyTypePurpose
statuses{ status: description }Maps status enum values to meanings
conflicts{ type: description }Maps conflict types to descriptions
restrictions{ type: description }Describes restriction types
windows{ term: definition }Terminology definitions
contentTypes{ type: behavior }Maps content types to their behavior
categoryExamples{ category: examples }Example values for categorization
example{ scenario, problem, result, solution }Concrete scenario with problem/solution
keyInsightstringCritical insight about the concept

You can invent new keys freely — just be descriptive. The formatting layer will include them in the rendered context as structured JSON.

Examples

Hierarchy concept with conventional keys:

examples/domain-knowledge-guide-02.ts
new DomainConcept({
  name: 'project_task_hierarchy',
  title: 'Project → Task → Tag',
  description:
    'Projects contain tasks, which in turn have tags. Tasks inherit certain fields from their parent project.',
  models: ['project', 'task', 'tag'],
  tags: ['hierarchy', 'structure', 'inheritance'],
  details: {
    inheritance: {
      from: 'project',
      to: 'task',
      fields: ['priority', 'due_date', 'assignee']
    },
    process: 'Create project first → add tasks under the project → assign tags per task',
    tips: [
      'Create the project before creating tasks — tasks are nested under projects',
      'Tag assignment is per-task, not per-project (use tags)'
    ]
  }
})
new DomainConcept({
  name: 'project_task_hierarchy',
  title: 'Project → Task → Tag',
  description:
    'Projects contain tasks, which in turn have tags. Tasks inherit certain fields from their parent project.',
  models: ['project', 'task', 'tag'],
  tags: ['hierarchy', 'structure', 'inheritance'],
  details: {
    inheritance: {
      from: 'project',
      to: 'task',
      fields: ['priority', 'due_date', 'assignee']
    },
    process: 'Create project first → add tasks under the project → assign tags per task',
    tips: [
      'Create the project before creating tasks — tasks are nested under projects',
      'Tag assignment is per-task, not per-project (use tags)'
    ]
  }
})

Status enum concept:

examples/domain-knowledge-guide-03.ts
new DomainConcept({
  name: 'task_completion_status',
  title: 'Task Status Lifecycle',
  description:
    'When a task is created or updated, its completion status follows a defined lifecycle.',
  models: ['task', 'project'],
  tags: ['status', 'lifecycle', 'tasks'],
  details: {
    statuses: {
      pending: 'Task has not been started yet',
      in_progress: 'Task is actively being worked on',
      completed: 'Task has been finished',
      cancelled: 'Task was abandoned before completion'
    },
    process:
      'Task created → status set to pending → in_progress when started → completed when done',
    tips: [
      'Task status transitions are tracked automatically — update status to reflect current state'
    ]
  }
})
new DomainConcept({
  name: 'task_completion_status',
  title: 'Task Status Lifecycle',
  description:
    'When a task is created or updated, its completion status follows a defined lifecycle.',
  models: ['task', 'project'],
  tags: ['status', 'lifecycle', 'tasks'],
  details: {
    statuses: {
      pending: 'Task has not been started yet',
      in_progress: 'Task is actively being worked on',
      completed: 'Task has been finished',
      cancelled: 'Task was abandoned before completion'
    },
    process:
      'Task created → status set to pending → in_progress when started → completed when done',
    tips: [
      'Task status transitions are tracked automatically — update status to reflect current state'
    ]
  }
})

Dynamic concept from model metadata:

src/create-mutability-concept.ts
export function createMutabilityConcept(modelClasses) {
  const readOnlyModels = Object.entries(modelClasses)
    .filter(([, M]) => M.readOnly)
    .map(([name]) => name)

  return new DomainConcept({
    name: 'model_mutability',
    title: 'Model Mutability: Read-Only Models and Immutable Fields',
    description: 'Some models are read-only lookups...',
    models: readOnlyModels,
    tags: ['infrastructure', 'read-only', 'mutability'],
    details: {
      readOnlyModels: { models: readOnlyModels },
      tips: ['Do not attempt to create or update read-only models']
    }
  })
}
export function createMutabilityConcept(modelClasses) {
  const readOnlyModels = Object.entries(modelClasses)
    .filter(([, M]) => M.readOnly)
    .map(([name]) => name)

  return new DomainConcept({
    name: 'model_mutability',
    title: 'Model Mutability: Read-Only Models and Immutable Fields',
    description: 'Some models are read-only lookups...',
    models: readOnlyModels,
    tags: ['infrastructure', 'read-only', 'mutability'],
    details: {
      readOnlyModels: { models: readOnlyModels },
      tips: ['Do not attempt to create or update read-only models']
    }
  })
}

Where to Put Concepts

Concepts live in a DomainModule alongside their related rules and workflows. Group by domain area (tasks, projects, books), not by component type — add each concept to the module for the area it describes:

src/concepts.ts
// src/<server>/domain/modules/projects.ts
import type { DomainModule } from '@mcp-rune/mcp-rune/domain'
import { DomainConcept } from '@mcp-rune/mcp-rune/domain'

export const projectModule: DomainModule = {
  concepts: [
    new DomainConcept({ name: 'project_task_hierarchy', ... }),
    new DomainConcept({ name: 'two_step_tag_assignment', ... }),
    new DomainConcept({ name: 'task_completion_status', ... }),
    new DomainConcept({ name: 'project_terminology', ... })
  ],
  rules: [/* rules about projects */],
  workflows: [/* project-related workflows */]
}
// src/<server>/domain/modules/projects.js
import { DomainConcept } from '@mcp-rune/mcp-rune/domain'

export const projectModule = {
  concepts: [
    new DomainConcept({ name: 'project_task_hierarchy', ... }),
    new DomainConcept({ name: 'two_step_tag_assignment', ... }),
    new DomainConcept({ name: 'task_completion_status', ... }),
    new DomainConcept({ name: 'project_terminology', ... })
  ],
  rules: [/* rules about projects */],
  workflows: [/* project-related workflows */]
}

BusinessRule — Declarative Constraints

Source: src/mcp/domain/business-rules.ts

A BusinessRule validates constraints between entities that individual model validation cannot express. Each rule is a data structure with an evaluate function — testable, auditable, introspectable.

Constructor

examples/domain-knowledge-guide-06.ts
import { BusinessRule } from '@mcp-rune/mcp-rune/domain'

new BusinessRule({
  name: 'task_requires_due_date', // Unique identifier (snake_case)
  description: 'Tasks in progress must have a due_date set',
  scope: ['task'], // Models this rule applies to
  severity: 'error', // 'error' | 'warning' | 'info'
  tags: ['in-progress', 'required-fields'], // For filtering
  evaluate(data, context = {}) {
    // Validation function
    // ...
  }
})
import { BusinessRule } from '@mcp-rune/mcp-rune/domain'

new BusinessRule({
  name: 'task_requires_due_date', // Unique identifier (snake_case)
  description: 'Tasks in progress must have a due_date set',
  scope: ['task'], // Models this rule applies to
  severity: 'error', // 'error' | 'warning' | 'info'
  tags: ['in-progress', 'required-fields'], // For filtering
  evaluate(data, context = {}) {
    // Validation function
    // ...
  }
})

The evaluate Function

The evaluate function receives two arguments and must return a result object:

Input:

ParameterDescription
dataThe entity data being validated (e.g., { status: 'in_progress', due_date: null })
contextAdditional context — related entities, parent data, content type, etc.

Output:

examples/domain-knowledge-guide-07.ts
{
  passed: true|false,        // Whether the rule passed
  message: 'Human-readable explanation',
  details: { ... },          // Optional: structured detail data
  suggestion: 'How to fix'   // Optional: actionable fix suggestion
}
{
  passed: true|false,        // Whether the rule passed
  message: 'Human-readable explanation',
  details: { ... },          // Optional: structured detail data
  suggestion: 'How to fix'   // Optional: actionable fix suggestion
}

Severity Levels

SeverityMeaningEffect on checkRules overall result
errorMust fix before proceedingFails the overall check
warningShould fix, but not blockingDoes NOT fail the overall check
infoInformational observationDoes NOT fail the overall check

Writing Good Rules

1. Early exit for non-applicable rules:

examples/domain-knowledge-guide-08.ts
evaluate(data) {
  if (data.status !== 'in_progress') {
    return { passed: true, message: 'Not an in-progress task, rule not applicable' }
  }
  // ... actual validation
}
evaluate(data) {
  if (data.status !== 'in_progress') {
    return { passed: true, message: 'Not an in-progress task, rule not applicable' }
  }
  // ... actual validation
}

2. Use context for cross-entity validation:

examples/domain-knowledge-guide-09.ts
evaluate(data, context = {}) {
  if (!context.tasks || context.tasks.length === 0) {
    return { passed: true, message: 'No tasks in context to validate against' }
  }
  // ... validate project dates encompass task due dates
}
evaluate(data, context = {}) {
  if (!context.tasks || context.tasks.length === 0) {
    return { passed: true, message: 'No tasks in context to validate against' }
  }
  // ... validate project dates encompass task due dates
}

3. Provide actionable suggestions:

examples/domain-knowledge-guide-10.ts
return {
  passed: false,
  message: `In-progress tasks require: ${missing.join(', ')}`,
  suggestion: 'Set due_date to a valid ISO date string (e.g., "2026-03-01T00:00:00Z")'
}
return {
  passed: false,
  message: `In-progress tasks require: ${missing.join(', ')}`,
  suggestion: 'Set due_date to a valid ISO date string (e.g., "2026-03-01T00:00:00Z")'
}

4. A rule can apply to multiple models:

examples/domain-knowledge-guide-11.ts
new BusinessRule({
  name: 'end_date_after_start_date',
  scope: ['task', 'project'] // Applies to both
  // ...
})
new BusinessRule({
  name: 'end_date_after_start_date',
  scope: ['task', 'project'] // Applies to both
  // ...
})

Examples

Field requirement rule:

src/missing.ts
new BusinessRule({
  name: 'task_requires_due_date',
  description: 'Tasks in progress must have a due_date set',
  scope: ['task'],
  severity: 'error',
  tags: ['in-progress', 'required-fields'],
  evaluate(data) {
    if (data.status !== 'in_progress') {
      return { passed: true, message: 'Not an in-progress task, rule not applicable' }
    }
    const missing = []
    if (!data.due_date) missing.push('due_date')

    if (missing.length > 0) {
      return {
        passed: false,
        message: `In-progress tasks require: ${missing.join(', ')}`,
        suggestion: 'Set due_date to a valid ISO date string (e.g., "2026-03-01T00:00:00Z")'
      }
    }
    return { passed: true, message: 'Task due date is set' }
  }
})
new BusinessRule({
  name: 'task_requires_due_date',
  description: 'Tasks in progress must have a due_date set',
  scope: ['task'],
  severity: 'error',
  tags: ['in-progress', 'required-fields'],
  evaluate(data) {
    if (data.status !== 'in_progress') {
      return { passed: true, message: 'Not an in-progress task, rule not applicable' }
    }
    const missing = []
    if (!data.due_date) missing.push('due_date')

    if (missing.length > 0) {
      return {
        passed: false,
        message: `In-progress tasks require: ${missing.join(', ')}`,
        suggestion: 'Set due_date to a valid ISO date string (e.g., "2026-03-01T00:00:00Z")'
      }
    }
    return { passed: true, message: 'Task due date is set' }
  }
})

Cross-entity validation rule (uses context):

src/project-dates-rule.ts
new BusinessRule({
  name: 'project_dates_encompass_tasks',
  description: "Project date range should encompass its tasks' due dates",
  scope: ['project'],
  severity: 'warning',
  tags: ['dates', 'cross-entity'],
  evaluate(data, context = {}) {
    if (!data.starts && !data.ends) {
      return { passed: true, message: 'Project has no date constraints' }
    }
    if (!context.tasks || context.tasks.length === 0) {
      return { passed: true, message: 'No tasks in context to validate against' }
    }

    const projectStart = data.starts ? new Date(data.starts) : null
    const issues = []

    for (const task of context.tasks) {
      if (projectStart && task.due_date && new Date(task.due_date) < projectStart) {
        issues.push(`Task due date is before project start`)
      }
    }

    if (issues.length > 0) {
      return {
        passed: false,
        message: issues.join('; '),
        suggestion: 'Extend project dates to encompass all task due dates'
      }
    }
    return { passed: true, message: 'Project dates encompass all task due dates' }
  }
})
new BusinessRule({
  name: 'project_dates_encompass_tasks',
  description: "Project date range should encompass its tasks' due dates",
  scope: ['project'],
  severity: 'warning',
  tags: ['dates', 'cross-entity'],
  evaluate(data, context = {}) {
    if (!data.starts && !data.ends) {
      return { passed: true, message: 'Project has no date constraints' }
    }
    if (!context.tasks || context.tasks.length === 0) {
      return { passed: true, message: 'No tasks in context to validate against' }
    }

    const projectStart = data.starts ? new Date(data.starts) : null
    const issues = []

    for (const task of context.tasks) {
      if (projectStart && task.due_date && new Date(task.due_date) < projectStart) {
        issues.push(`Task due date is before project start`)
      }
    }

    if (issues.length > 0) {
      return {
        passed: false,
        message: issues.join('; '),
        suggestion: 'Extend project dates to encompass all task due dates'
      }
    }
    return { passed: true, message: 'Project dates encompass all task due dates' }
  }
})

Where to Put Rules

Rules live in a DomainModule alongside their related concepts and workflows. Group by domain area (tasks, projects, books), not by component type:

src/task-module.ts
// src/<server>/domain/modules/tasks.ts
import type { DomainModule } from '@mcp-rune/mcp-rune/domain'
import { BusinessRule, DomainConcept } from '@mcp-rune/mcp-rune/domain'

export const taskModule: DomainModule = {
  concepts: [/* concepts about tasks */],
  rules: [
    new BusinessRule({ name: 'task_requires_due_date', ... }),
    new BusinessRule({ name: 'task_priority_valid', ... }),
  ],
  workflows: [/* task-related workflows */],
}
import { BusinessRule } from '@mcp-rune/mcp-rune/domain';
export const taskModule = {
    concepts: [ /* concepts about tasks */],
    rules: [
        new BusinessRule({ name: 'task_requires_due_date', ... }),
        new BusinessRule({ name: 'task_priority_valid', ... }),
    ],
    workflows: [ /* task-related workflows */],
};

WorkflowDefinition — Multi-Step Guides

Source: src/mcp/domain/workflows.ts

A WorkflowDefinition is a structured multi-step process guide. It can include tool calls, decision points, and tips. Two common patterns:

  • Onboarding/Setup — How-to guides for accomplishing a task
  • Demo — Choreographed presentations with audience-friendly narration (tagged demo)

Constructor

examples/domain-knowledge-guide-15.ts
import { WorkflowDefinition } from '@mcp-rune/mcp-rune/domain'

new WorkflowDefinition({
  name: 'create_project_workflow',                    // Unique identifier (snake_case)
  title: 'Set Up a New Project',               // Human-readable title
  description: 'Creates a project and populates it with tasks.',
  tags: ['project', 'tasks', 'onboarding'],    // For filtering
  models: ['project', 'task', 'tag'],          // Models involved
  steps: [
    { order: 1, title: '...', description: '...', ... },
    { order: 2, title: '...', description: '...', ... },
  ]
})
import { WorkflowDefinition } from '@mcp-rune/mcp-rune/domain'

new WorkflowDefinition({
  name: 'create_project_workflow',                    // Unique identifier (snake_case)
  title: 'Set Up a New Project',               // Human-readable title
  description: 'Creates a project and populates it with tasks.',
  tags: ['project', 'tasks', 'onboarding'],    // For filtering
  models: ['project', 'task', 'tag'],          // Models involved
  steps: [
    { order: 1, title: '...', description: '...', ... },
    { order: 2, title: '...', description: '...', ... },
  ]
})

WorkflowStep

Each step is a plain object (auto-wrapped in WorkflowStep):

examples/domain-knowledge-guide-16.ts
{
  order: 1,                           // Step number (1-based)
  title: 'Find the project',          // Step title
  description: 'Search for the project you want to set up.',
  tool: 'find_records',               // Optional: MCP tool to call
  toolArgs: {                         // Optional: example arguments
    model: 'project',
    search: { name: '<project_name>' }
  },
  decision: {                          // Optional: branching point
    question: 'All tasks or specific ones?',
    options: [
      { label: 'All tasks', description: 'Skip — project applies to all tasks' },
      { label: 'Specific tasks', description: 'Add task records', nextStep: 6 }
    ]
  },
  tips: [                              // Optional: guidance for this step
    'You need the project ID for the next step',
    'If the project does not exist, create it first'
  ]
}
{
  order: 1,                           // Step number (1-based)
  title: 'Find the project',          // Step title
  description: 'Search for the project you want to set up.',
  tool: 'find_records',               // Optional: MCP tool to call
  toolArgs: {                         // Optional: example arguments
    model: 'project',
    search: { name: '<project_name>' }
  },
  decision: {                          // Optional: branching point
    question: 'All tasks or specific ones?',
    options: [
      { label: 'All tasks', description: 'Skip — project applies to all tasks' },
      { label: 'Specific tasks', description: 'Add task records', nextStep: 6 }
    ]
  },
  tips: [                              // Optional: guidance for this step
    'You need the project ID for the next step',
    'If the project does not exist, create it first'
  ]
}

Notes:

  • tool references an MCP tool name. toolArgs are example arguments shown to users, not auto-executed.
  • decision.options[].nextStep creates branching — users can jump to a different step.
  • Steps without a tool are narrative/explanation steps (common in demo workflows).

Examples

Onboarding workflow:

examples/domain-knowledge-guide-17.ts
new WorkflowDefinition({
  name: 'create_project_workflow',
  title: 'Set Up a New Project',
  description: 'Set up a project and populate it with tasks.',
  tags: ['project', 'tasks', 'onboarding'],
  models: ['project', 'task', 'tag'],
  steps: [
    {
      order: 1,
      title: 'Find the project',
      description: 'Search for the project you want to set up.',
      tool: 'find_records',
      toolArgs: { model: 'project', search: { name: '<project_name>' } },
      tips: ['You need the project ID for the next step']
    },
    {
      order: 2,
      title: 'Get the task creation guide',
      description: 'Load the guided creation form for tasks.',
      tool: 'get_prompt_guide',
      toolArgs: { guide_name: 'create_task' },
      tips: ['Use mode: "quick" if you already know the field values']
    }
    // ... more steps
  ]
})
new WorkflowDefinition({
  name: 'create_project_workflow',
  title: 'Set Up a New Project',
  description: 'Set up a project and populate it with tasks.',
  tags: ['project', 'tasks', 'onboarding'],
  models: ['project', 'task', 'tag'],
  steps: [
    {
      order: 1,
      title: 'Find the project',
      description: 'Search for the project you want to set up.',
      tool: 'find_records',
      toolArgs: { model: 'project', search: { name: '<project_name>' } },
      tips: ['You need the project ID for the next step']
    },
    {
      order: 2,
      title: 'Get the task creation guide',
      description: 'Load the guided creation form for tasks.',
      tool: 'get_prompt_guide',
      toolArgs: { guide_name: 'create_task' },
      tips: ['Use mode: "quick" if you already know the field values']
    }
    // ... more steps
  ]
})

Demo workflow:

examples/domain-knowledge-guide-18.ts
new WorkflowDefinition({
  name: 'demo_task_tracking',
  title: 'Demo: Task Tracking',
  description: 'Choreographed demo with audience-friendly explanations.',
  tags: ['tasks', 'demo'],
  models: ['project', 'task'],
  steps: [
    {
      order: 1,
      title: 'Set the scene',
      description:
        'Explain task tracking: "Tasks let you break down projects into actionable steps..."',
      tips: ['Key value prop: tasks make large goals manageable']
      // No tool — narrative step
    },
    {
      order: 2,
      title: 'Find a demo project',
      description: 'Search for a recognizable project.',
      tool: 'find_records',
      toolArgs: { model: 'project', search: { name: 'Website Redesign' } },
      tips: ['Choose a project the audience will recognize']
    }
    // ... more steps
  ]
})
new WorkflowDefinition({
  name: 'demo_task_tracking',
  title: 'Demo: Task Tracking',
  description: 'Choreographed demo with audience-friendly explanations.',
  tags: ['tasks', 'demo'],
  models: ['project', 'task'],
  steps: [
    {
      order: 1,
      title: 'Set the scene',
      description:
        'Explain task tracking: "Tasks let you break down projects into actionable steps..."',
      tips: ['Key value prop: tasks make large goals manageable']
      // No tool — narrative step
    },
    {
      order: 2,
      title: 'Find a demo project',
      description: 'Search for a recognizable project.',
      tool: 'find_records',
      toolArgs: { model: 'project', search: { name: 'Website Redesign' } },
      tips: ['Choose a project the audience will recognize']
    }
    // ... more steps
  ]
})

Where to Put Workflows

Workflows live in a DomainModule alongside their related concepts and rules. Group related workflows in the same module (e.g., setup + demo variant of the same process):

src/project-workflows.ts
// src/<server>/domain/modules/projects.ts
import type { DomainModule } from '@mcp-rune/mcp-rune/domain'
import { WorkflowDefinition } from '@mcp-rune/mcp-rune/domain'

export const projectModule: DomainModule = {
  concepts: [/* concepts about projects */],
  rules: [/* rules about projects */],
  workflows: [
    new WorkflowDefinition({ ... }),  // Setup
    new WorkflowDefinition({ ... })   // Demo
  ]
}
// src/<server>/domain/modules/projects.js
import { WorkflowDefinition } from '@mcp-rune/mcp-rune/domain'

export const projectModule = {
  concepts: [/* concepts about projects */],
  rules: [/* rules about projects */],
  workflows: [
    new WorkflowDefinition({ ... }),  // Setup
    new WorkflowDefinition({ ... })   // Demo
  ]
}

Wiring It Up — The Domain Registry

Source: src/mcp/domain/registry.ts

The DomainRegistry is a facade over a DomainAdapter. It aggregates domain intelligence into a single injectable dependency.

Creating the Registry

src/registries/create-my-domain-registry.ts
// src/<server>/domain/registry.ts
import { DomainRegistry, InMemoryDomainAdapter } from '@mcp-rune/mcp-rune/domain'
import type { DomainModule } from '@mcp-rune/mcp-rune/domain'

import { taskModule } from './modules/tasks.js'
import { projectModule } from './modules/projects.js'

export function createMyDomainRegistry(): DomainRegistry {
  return new DomainRegistry({
    adapter: new InMemoryDomainAdapter([taskModule, projectModule])
  })
}
// src/<server>/domain/registry.ts
import { DomainRegistry, InMemoryDomainAdapter } from '@mcp-rune/mcp-rune/domain'
import { taskModule } from './modules/tasks.js'
import { projectModule } from './modules/projects.js'
export function createMyDomainRegistry() {
  return new DomainRegistry({
    adapter: new InMemoryDomainAdapter([taskModule, projectModule])
  })
}

Each module lives in its own file and groups related concepts, rules, and workflows:

src/modules/tasks.ts
// src/<server>/domain/modules/tasks.ts
import type { DomainModule } from '@mcp-rune/mcp-rune/domain'
import { DomainConcept, BusinessRule, WorkflowDefinition } from '@mcp-rune/mcp-rune/domain'

export const taskModule: DomainModule = {
  concepts: [
    new DomainConcept({ name: 'task_lifecycle', ... }),
  ],
  rules: [
    new BusinessRule({ name: 'task_requires_due_date', ... }),
  ],
  workflows: [
    new WorkflowDefinition({ name: 'setup_project_with_tasks', ... }),
  ],
}
import { DomainConcept, BusinessRule, WorkflowDefinition } from '@mcp-rune/mcp-rune/domain';
export const taskModule = {
    concepts: [
        new DomainConcept({ name: 'task_lifecycle', ... }),
    ],
    rules: [
        new BusinessRule({ name: 'task_requires_due_date', ... }),
    ],
    workflows: [
        new WorkflowDefinition({ name: 'setup_project_with_tasks', ... }),
    ],
};

Injecting into the Tool Registry

Pass the domain registry when creating the tool registry. The tool registry injects it into every domain tool instance:

src/registries/domain-registry.ts
// src/<server>/config.ts
import { DATA_TOOL_CLASSES, DOMAIN_TOOL_CLASSES, ToolRegistry } from '@mcp-rune/mcp-rune/tools'

const domainRegistry = createMyDomainRegistry()

const toolRegistry = new ToolRegistry({
  toolClasses: { ...DATA_TOOL_CLASSES, ...DOMAIN_TOOL_CLASSES },
  models: MODEL_CLASSES,
  domainRegistry, // <-- injected here
  createApiClient
})
// src/<server>/config.js
import { DATA_TOOL_CLASSES, DOMAIN_TOOL_CLASSES, ToolRegistry } from '@mcp-rune/mcp-rune/tools'

const domainRegistry = createMyDomainRegistry()

const toolRegistry = new ToolRegistry({
  toolClasses: { ...DATA_TOOL_CLASSES, ...DOMAIN_TOOL_CLASSES },
  models: MODEL_CLASSES,
  domainRegistry, // <-- injected here
  createApiClient
})

Graceful Absence

If domainRegistry is undefined (not configured for this server), the four domain tools are simply filtered out of the tool list. No errors — other tools work normally. This allows the framework to be optional per server.


How Domain Tools Consume the Registry

Four tools expose domain intelligence to users. All extend BaseDomainTool, which sets the static booleans requiresAuth = false and requiresDomainRegistry = true.

get_domain_context

Retrieves composed context for a model or concept.

InputBehavior
No argsLists all concepts and workflows (overview)
{ model: 'task' }Returns field metadata + concepts + rules + workflows for the model
{ concept: 'project_task_hierarchy' }Returns full concept details
{ model: 'task', concept: 'completion' }Returns both model context and concept search results

Formatting of details conventional keys:

  • details.inheritance → rendered as **Inheritance:** from → to (fields: ...)
  • details.process → rendered as **Process:** ...
  • details.tips → rendered as bulleted list under **Tips:**
  • All other keys → included as structured JSON in the context

check_business_rules

Validates entity data against applicable business rules.

InputBehavior
{ model: 'task', data: { status: 'in_progress' } }Evaluates all rules scoped to task
{ model: 'project', data: {...}, context: { tasks: [...] } }Evaluates with cross-entity context

Output is grouped by severity: Errors (must fix) → Warnings (should fix) → Info → Passed.

suggest_workflow

Returns a workflow roadmap (all step titles) plus the first step in full detail. The LLM executes one step at a time, calling get_workflow_step for each subsequent step.

InputBehavior
No argsLists all workflows
{ goal: 'project tasks' }Searches by title/description/tags, returns best match
{ workflow: 'create_project_workflow' }Returns the full roadmap + first step for a named workflow
{ tag: 'demo' }Filters by tag

get_workflow_step

Returns detailed instructions for a single workflow step — the tool to call, arguments to pass, tips, and exclusion warnings. For loop and parallel groups, returns all steps in the group together with a hint about the next step after the group.

InputBehavior
{ workflow: 'create_project_workflow', step: 1 }Detail for step 1: tool, args, tips, next-step hint
{ workflow: 'create_project_workflow', step: 3 }If step 3 is in a loop or parallel group, returns the full group
{ workflow: 'unknown', step: 1 }Error: lists available workflow names
{ workflow: 'create_project_workflow', step: 99 }Error: lists the workflow’s available step numbers

The tool is stateless. The LLM (or a coordinating agent) drives progression by calling get_workflow_step once per step; the framework never tracks “current step” on the server side. Steps may declare a contextHint payload that surfaces in the response’s _meta.contextHints for the transient-context protocol.


Step-by-Step: Adding Domain Intelligence to a New Server

If your server doesn’t have domain intelligence yet, follow these steps:

1. Create the domain directory structure

src/<server>/domain/ ├── registry.js ├── knowledge/ │ └── concepts.js ├── rules/ │ └── <domain>-rules.js └── workflows/ └── <category>.js

One DomainModule file per domain area — each bundles the concepts, rules, and workflows for that area. Steps 2–4 fill in one module file (src/<server>/domain/modules/my-area.ts); step 5 bundles the arrays and wires the registry.

2. Define concepts

src/concepts.ts
// src/<server>/domain/modules/my-area.ts — the module's concepts
import { DomainConcept } from '@mcp-rune/mcp-rune/domain'

const concepts = [
  new DomainConcept({
    name: 'your_concept_name',
    title: 'Human-Readable Title',
    description: 'What this concept explains.',
    models: ['model_a', 'model_b'],
    tags: ['relevant', 'tags'],
    details: {
      process: 'Step 1 → Step 2 → Step 3',
      tips: ['Helpful tip 1', 'Helpful tip 2']
    }
  })
]
// src/<server>/domain/modules/my-area.js — the module's concepts
import { DomainConcept } from '@mcp-rune/mcp-rune/domain'

const concepts = [
  new DomainConcept({
    name: 'your_concept_name',
    title: 'Human-Readable Title',
    description: 'What this concept explains.',
    models: ['model_a', 'model_b'],
    tags: ['relevant', 'tags'],
    details: {
      process: 'Step 1 → Step 2 → Step 3',
      tips: ['Helpful tip 1', 'Helpful tip 2']
    }
  })
]

3. Define business rules

src/my-rules.ts
// src/<server>/domain/modules/my-area.ts — the module's rules
import { BusinessRule } from '@mcp-rune/mcp-rune/domain'

const rules = [
  new BusinessRule({
    name: 'my_validation_rule',
    description: 'What this rule checks',
    scope: ['model_a'],
    severity: 'error',
    tags: ['validation'],
    evaluate(data) {
      if (!data.required_field) {
        return {
          passed: false,
          message: 'required_field is missing',
          suggestion: 'Set required_field to a valid value'
        }
      }
      return { passed: true, message: 'required_field is present' }
    }
  })
]
// src/<server>/domain/modules/my-area.js — the module's rules
import { BusinessRule } from '@mcp-rune/mcp-rune/domain'

const rules = [
  new BusinessRule({
    name: 'my_validation_rule',
    description: 'What this rule checks',
    scope: ['model_a'],
    severity: 'error',
    tags: ['validation'],
    evaluate(data) {
      if (!data.required_field) {
        return {
          passed: false,
          message: 'required_field is missing',
          suggestion: 'Set required_field to a valid value'
        }
      }
      return { passed: true, message: 'required_field is present' }
    }
  })
]

4. Define workflows

src/my-workflows.ts
// src/<server>/domain/modules/my-area.ts — the module's workflows
import { WorkflowDefinition } from '@mcp-rune/mcp-rune/domain'

const workflows = [
  new WorkflowDefinition({
    name: 'my_setup_workflow',
    title: 'Set Up Feature X',
    description: 'Guides users through setting up Feature X.',
    tags: ['onboarding'],
    models: ['model_a', 'model_b'],
    steps: [
      {
        order: 1,
        title: 'Find the parent entity',
        description: 'Search for the entity to configure.',
        tool: 'find_records',
        toolArgs: { model: 'model_a', search: { name: '<name>' } },
        tips: ['You need the entity ID for the next step']
      },
      {
        order: 2,
        title: 'Create the child entity',
        description: 'Create a child entity linked to the parent.',
        tool: 'create_model',
        toolArgs: { model: 'model_b', attributes: { parent_id: '<from step 1>' } },
        tips: ['Check business rules first with check_business_rules']
      }
    ]
  })
]
// src/<server>/domain/modules/my-area.js — the module's workflows
import { WorkflowDefinition } from '@mcp-rune/mcp-rune/domain'

const workflows = [
  new WorkflowDefinition({
    name: 'my_setup_workflow',
    title: 'Set Up Feature X',
    description: 'Guides users through setting up Feature X.',
    tags: ['onboarding'],
    models: ['model_a', 'model_b'],
    steps: [
      {
        order: 1,
        title: 'Find the parent entity',
        description: 'Search for the entity to configure.',
        tool: 'find_records',
        toolArgs: { model: 'model_a', search: { name: '<name>' } },
        tips: ['You need the entity ID for the next step']
      },
      {
        order: 2,
        title: 'Create the child entity',
        description: 'Create a child entity linked to the parent.',
        tool: 'create_model',
        toolArgs: { model: 'model_b', attributes: { parent_id: '<from step 1>' } },
        tips: ['Check business rules first with check_business_rules']
      }
    ]
  })
]

5. Bundle the module and assemble the registry

At the bottom of the module file, bundle the three arrays from steps 2–4 into one DomainModule:

src/my-module.ts
// src/<server>/domain/modules/my-area.ts
import type { DomainModule } from '@mcp-rune/mcp-rune/domain'

export const myAreaModule: DomainModule = { concepts, rules, workflows }
// src/<server>/domain/modules/my-area.js
export const myAreaModule = { concepts, rules, workflows }

Then wrap the modules in an adapter and registry:

src/registries/create-my-domain-registry.ts
// src/<server>/domain/registry.ts
import { DomainRegistry, InMemoryDomainAdapter } from '@mcp-rune/mcp-rune/domain'
import { MODEL_CLASSES } from '../models/index.js'

import { myAreaModule } from './modules/my-area.js'

export function createMyDomainRegistry(): DomainRegistry {
  return new DomainRegistry({
    adapter: new InMemoryDomainAdapter([myAreaModule]),
    models: MODEL_CLASSES
  })
}
// src/<server>/domain/registry.js
import { DomainRegistry, InMemoryDomainAdapter } from '@mcp-rune/mcp-rune/domain'
import { MODEL_CLASSES } from '../models/index.js'

import { myAreaModule } from './modules/my-area.js'

export function createMyDomainRegistry() {
  return new DomainRegistry({
    adapter: new InMemoryDomainAdapter([myAreaModule]),
    models: MODEL_CLASSES
  })
}

6. Inject into the server config

src/registries/inject-domain-registry.ts
// src/<server>/config.ts
import { DATA_TOOL_CLASSES, DOMAIN_TOOL_CLASSES, ToolRegistry } from '@mcp-rune/mcp-rune/tools'

import { createMyDomainRegistry } from './domain/registry.js'

const domainRegistry = createMyDomainRegistry()

const toolRegistry = new ToolRegistry({
  toolClasses: { ...DATA_TOOL_CLASSES, ...DOMAIN_TOOL_CLASSES },
  models: MODEL_CLASSES,
  domainRegistry,
  createApiClient
})
// src/<server>/config.js
import { DATA_TOOL_CLASSES, DOMAIN_TOOL_CLASSES, ToolRegistry } from '@mcp-rune/mcp-rune/tools'

import { createMyDomainRegistry } from './domain/registry.js'

const domainRegistry = createMyDomainRegistry()

const toolRegistry = new ToolRegistry({
  toolClasses: { ...DATA_TOOL_CLASSES, ...DOMAIN_TOOL_CLASSES },
  models: MODEL_CLASSES,
  domainRegistry,
  createApiClient
})

That’s it. The four domain tools will automatically appear in the tool list and serve your domain knowledge, rules, and workflows.


Domain search defaults to case-insensitive substring matching — instant, no model loading. Embedding-based semantic search is opt-in per registry via domainRegistry.initSearch('embedding'): queries and items are converted to 384-dimensional vectors and compared by cosine similarity. This enables natural language queries, synonym matching (“permissions” finds “rights”), and ranked results.

How It Works

  1. At server startup, domainRegistry.initSearch('embedding') is called (fire-and-forget)
  2. Each registry (knowledge, workflows) converts its items to text using a text function
  3. Text is embedded into 384-dim vectors via embedBatch() (local MiniLM-L6-v2, no API keys)
  4. At search time, the query is embedded via embed() and compared against all pre-computed vectors
  5. Results above a similarity threshold are returned, ranked by score (highest first)

The SemanticSearch Class

src/mcp/domain/semantic-search.ts is a composable utility — each registry gets its own instance:

src/search.ts
import { SemanticSearch } from '@mcp-rune/mcp-rune/domain'

const search = new SemanticSearch({ threshold: 0.3, topK: 20 })
await search.initialize(items, (item) => `${item.name} ${item.title}: ${item.description}`)

const results = await search.search('project hierarchy')
// [{ item: <DomainConcept>, score: 0.72 }, ...]
import { SemanticSearch } from '@mcp-rune/mcp-rune/domain'

const search = new SemanticSearch({ threshold: 0.3, topK: 20 })
await search.initialize(items, (item) => `${item.name} ${item.title}: ${item.description}`)

const results = await search.search('project hierarchy')
// [{ item: <DomainConcept>, score: 0.72 }, ...]
MethodDescription
initialize(items, textFn)Pre-compute embeddings for all items
search(query, options?)Returns { item, score }[] ranked by cosine similarity
isInitializedBoolean guard for fallback logic

Graceful Fallback

If initSearch('embedding') hasn’t completed yet (startup warmup ~2-5s) or failed, search methods fall back to substring matching automatically. Domain tools always work, even without the embedding model.

src/results.ts
async searchConcepts(query) {
  if (this._semanticSearch.isInitialized) {
    const results = await this._semanticSearch.search(query)
    if (results.length > 0) return results.map((r) => r.item)
  }
  return this._substringSearch(query)  // fallback
}
async searchConcepts(query) {
  if (this._semanticSearch.isInitialized) {
    const results = await this._semanticSearch.search(query)
    if (results.length > 0) return results.map((r) => r.item)
  }
  return this._substringSearch(query)  // fallback
}

Text Representation

Each registry converts items to searchable text:

RegistryText Format
Concepts{name} {title}: {description} {tags joined by space}
Workflows{name} {title}: {description} {tags joined by space}

Configuration

OptionDefaultDescription
threshold0.3Minimum cosine similarity to include in results
topK10Maximum results (same default for concepts and workflows; overridable per instance and per search call)

Why threshold 0.3: MiniLM-L6-v2 normalized embeddings produce lower similarity scores than larger models. A threshold of 0.3 catches semantically relevant results without noise. The threshold is configurable per-instance and overridable per-search call.

Why in-memory, not pgvector: The domain dataset is small (~60 items). Computing cosine similarity over 60 vectors takes microseconds. No external database needed. pgvector is reserved for the larger, growing CRUD operation history.

Initialization

examples/domain-knowledge-guide-33.ts
// src/<server>/config.ts — fire-and-forget, same pattern as vector storage and tracing
domainRegistry
  .initSearch('embedding')
  .then(() => logger.info('Domain semantic search initialized', { service: 'mcp-config' }))
  .catch((err) =>
    logger.warn('Domain semantic search unavailable, using substring fallback', {
      service: 'mcp-config',
      error: err.message
    })
  )
// src/<server>/config.js — fire-and-forget, same pattern as vector storage and tracing
domainRegistry
  .initSearch('embedding')
  .then(() => logger.info('Domain semantic search initialized', { service: 'mcp-config' }))
  .catch((err) =>
    logger.warn('Domain semantic search unavailable, using substring fallback', {
      service: 'mcp-config',
      error: err.message
    })
  )

Testing

Unit Testing Rules

Rules are pure functions — test them directly:

src/rule.ts
import { describe, it, expect } from 'vitest'
import { myAreaModule } from '../src/<server>/domain/modules/my-area.js'

describe('my_validation_rule', () => {
  const rule = myAreaModule.rules.find((r) => r.name === 'my_validation_rule')

  it('passes when required_field is present', () => {
    const result = rule.evaluate({ required_field: 'value' })
    expect(result.passed).toBe(true)
  })

  it('fails when required_field is missing', () => {
    const result = rule.evaluate({})
    expect(result.passed).toBe(false)
    expect(result.suggestion).toBeDefined()
  })

  it('uses context for cross-entity validation', () => {
    const result = rule.evaluate({ starts: '2025-01-01' }, { rights: [{ starts: '2024-12-01' }] })
    expect(result.passed).toBe(false)
  })
})
import { describe, it, expect } from 'vitest'
import { myAreaModule } from '../src/<server>/domain/modules/my-area.js'

describe('my_validation_rule', () => {
  const rule = myAreaModule.rules.find((r) => r.name === 'my_validation_rule')

  it('passes when required_field is present', () => {
    const result = rule.evaluate({ required_field: 'value' })
    expect(result.passed).toBe(true)
  })

  it('fails when required_field is missing', () => {
    const result = rule.evaluate({})
    expect(result.passed).toBe(false)
    expect(result.suggestion).toBeDefined()
  })

  it('uses context for cross-entity validation', () => {
    const result = rule.evaluate({ starts: '2025-01-01' }, { rights: [{ starts: '2024-12-01' }] })
    expect(result.passed).toBe(false)
  })
})

Integration Testing the Registry

src/registry.ts
import { describe, it, expect } from 'vitest'
import { createMyDomainRegistry } from '../src/<server>/domain/registry.js'

describe('domain registry', () => {
  const registry = createMyDomainRegistry()

  it('provides context for known models', async () => {
    const context = await registry.getContextForModel('model_a')
    expect(context.model).toBe('model_a')
    expect(context.concepts.length).toBeGreaterThan(0)
  })

  it('finds concepts by search', async () => {
    const results = await registry.searchConcepts('hierarchy')
    expect(results.length).toBeGreaterThan(0)
  })

  it('evaluates rules for a model', async () => {
    const result = await registry.checkRules('model_a', { required_field: 'value' })
    expect(result.passed).toBe(true)
  })
})
import { describe, it, expect } from 'vitest'
import { createMyDomainRegistry } from '../src/<server>/domain/registry.js'

describe('domain registry', () => {
  const registry = createMyDomainRegistry()

  it('provides context for known models', async () => {
    const context = await registry.getContextForModel('model_a')
    expect(context.model).toBe('model_a')
    expect(context.concepts.length).toBeGreaterThan(0)
  })

  it('finds concepts by search', async () => {
    const results = await registry.searchConcepts('hierarchy')
    expect(results.length).toBeGreaterThan(0)
  })

  it('evaluates rules for a model', async () => {
    const result = await registry.checkRules('model_a', { required_field: 'value' })
    expect(result.passed).toBe(true)
  })
})

Test File Naming

Test files must use the .spec.ts extension (not .test.ts). Place them in __tests__/ mirroring the source structure.