On this page5 sections
Philosophy
Inscribe small. Cast large.
mcp-rune is opinionated, model-driven, and unapologetically Rails-flavored. This page collects the worldview that shapes every API decision in the framework — the why behind the conventions, the diagram of layers it generates, and the principles the codebase is held to.
Why “mcp-rune”?
A rune is a compact, declarative symbol that produces powerful effects far larger than itself. In mcp-rune, everything you write is a rune.
You inscribe declarations:
- a model — what your domain looks like
- a prompt — how the LLM should reason about it
- an app — how humans interact with it
- a workflow — how operations chain together
Each inscription fits on one screen. The framework is the runic system: an alphabet of conventions (BaseModel, BasePrompt, BaseAppForm, DomainRegistry) and a casting engine that turns those inscriptions into a complete MCP server — tools, validation pipelines, UI, OAuth, observability, documentation.
The model is the foundational rune. Prompts, tools, apps, forms, search, retrieval, and docs all derive their structure from it — the framework’s single source of truth. Each other kind of inscription then adds its own dimension: prompts add reasoning, apps add interaction, workflows add orchestration.
Architecture
mcp-rune is a two-tier system. You author a thin shell of declarations; the framework provides the engine that turns those declarations into a working MCP server.
your-server/ (you write this)
│
├─ models/ Model definitions (BaseModel subclasses)
├─ prompts/ Prompt classes (fieldGroups + strategy)
├─ tools/ Custom tools (extend BaseTool)
├─ apps/ Custom apps (extend BaseAppForm, optional)
├─ domain/ Workflows, rules, knowledge
└─ server.ts StdioServer or HttpServer entry point
mcp-rune/ (the framework)
│
├─ src/mcp/
│ ├─ models/ What a Model IS — BaseModel + the kinds/ registry
│ ├─ model-layer/ What CONSUMES a Model — per-model-bound reads
│ ├─ data-layer/ Backend I/O seam — DataLayer + adapters + api-extensions
│ ├─ analysis-layer/ Per-model analysis projections (edges, embeddings)
│ ├─ prompts/ What a Prompt IS (BasePrompt + builders + generators) plus the registry/cache/validator that consume it (file-name split)
│ ├─ apps/ Schema-driven interactive UIs
│ ├─ tools/ Polymorphic CRUD + form-strategy tools
│ ├─ domain/ Concepts, business rules, workflows
│ ├─ extensions/ HttpExtension / ToolFlowExtension hooks
│ └─ server-factory.ts createServer composition root
├─ src/runtime/ Logger, tracing, error tracking
├─ src/oauth2/ OAuthService, token store, RFC-compliant adapters
└─ src/db/ PostgreSQL client (analysis layer)
A rendered version of this stack lives on the mcp-rune.dev landing page.
Three peer per-model layers
Tools, apps, prompts, and api-extensions reach the rest of the framework through three peer interfaces, each one bound (or rebound) per model and per request as needed.
┌──────────────────────────┐
│ Tool / App / Prompt │ (projection layer — what you write)
└────┬──────────┬────────┬─┘
│ │ │
this.dataLayer this.modelLayer this.analysisLayer
(per request) (name) per call (name) per call
│ │ │
┌──────────▼──┐ ┌────▼────┐ ┌▼─────────────┐
│ DataLayer │ │ Model- │ │ Analysis- │
│ backend │ │ Layer │ │ Layer │
│ I/O │ │ reads │ │ projections │
└──────────┬──┘ └────┬────┘ └──┬───────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────┐
│ Model declaration │ (src/mcp/models/)
└──────────────────────────┘
DataLayer— per-authenticated-request backend I/O. Tools and apps callthis.dataLayer.find(),.list(),.searchNormalized(),.dispatch(). Built-in implementations: in-memory stub,ModelService(HTTP viaApiClient),SearchEnabledDataLayerwrapper.ModelLayer— per-model-bound, synchronous reads against the model’s static config. Surface:resolveDerivedFields(records). Constructed viamodelLayer(name); no I/O, no DB. Kind descriptors are shared vocabulary read directly fromsrc/mcp/models/kinds/(ADR 0009), not a layer method.AnalysisLayer— per-model-bound, per-request analysis projections that carry the authenticatedDataLayerfor any I/O they need. Surface today:extractEdges(record),buildEmbeddingText(record),walkHops(rootRecords, options). Designed to hostsummarizeandbuildStratifiernext.
Projection-layer code never imports the underlying helpers directly. For layer-internal helpers (resolveDerivedFields, edge extraction, multi-hop fetch, …) the boundary is enforced by no-restricted-imports in eslint.config.js; keeping ModelService and ApiClient behind the DataLayer interface is a convention documented in AGENTS.md, which holds the canonical rules for both.
Definition vs consumption
The folder layout reflects a deliberate split: what a thing IS lives separately from what consumes it.
| Declaration | Consumption |
|---|---|
src/mcp/models/ | src/mcp/model-layer/, src/mcp/data-layer/, src/mcp/analysis-layer/ |
src/mcp/prompts/ declaration files | src/mcp/prompts/ consumption files (prompt-registry, prompt-cache, prompt-validator, form-strategies/) |
models/ holds base-model.ts, model-definitions.ts, and the kinds/ registry — purely descriptive. Helpers that read or transform a model belong in model-layer/ or its peers. The prompt subsystem applies the same dichotomy by file-name convention inside one folder. When future seams arrive (an auth-layer, a workflow-layer), they will follow the same pattern: consumption code kept clearly apart from the declarations it reads — by sibling folder or by file-name convention — never new helpers dumped into the declaration side.
Design principles
The framework is held to seven invariants. They are the test you can apply to any proposed API change:
- Model is the single source of truth — model declarations drive tools, prompts, forms, and docs. Change the model, everything downstream re-derives.
- Convention over configuration — sensible defaults, override when needed. The default path should always be the short path.
- Polymorphic tools — 9 bundled tools (6 CRUD + 3 form-strategy) serve all models, keeping LLM context clean. Adding the eleventh model does not add a tenth tool.
- Declarative auth — tools declare static booleans (
requiresAuth,requiresVectorStorage,requiresDomainRegistry,requiresPromptRegistry) and the registry wires auth and services from them. You never wire auth imperatively per tool call. - API-agnostic — pluggable
DataLayer, conventions, and search request shapers for any REST backend. The framework knows nothing about your wire format until you tell it. - Layer discipline — projection-layer code consumes
DataLayer,ModelLayer,AnalysisLayeronly. Concrete adapters and internal helpers stay behind the interface. The eslint guard makes this a build error, not a guideline. - Pure framework — zero domain knowledge; your server adds the domain. mcp-rune ships no built-in business concepts.
See Why mcp-rune? for how these principles compare against alternative MCP framework choices. Once you’re convinced this is the right shape, the Quickstart puts a real server in front of you in under ten minutes.