mikser-io 6.27.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  **Mikser is the content layer of your application.** Business logic, user accounts, transactions live in their own services; mikser handles the parts that *are* content — pages, docs, the published catalog, multi-format outputs. [Vue](https://github.com/almero-digital-marketing/mikser-io-sdk-vue), [React](https://github.com/almero-digital-marketing/mikser-io-sdk-react), and [Svelte](https://github.com/almero-digital-marketing/mikser-io-sdk-svelte) SDKs are the seam between them — same surface across all three (`useDocument`, `useDocuments`, multilingual `useHref`, live SSE updates), each in its framework's idiomatic shape.
8
8
 
9
- Built for Node.js around a strict lifecycle, a composable plugin system, and direct control over every output. Every document, asset, and template flows through the same deterministic pipeline. Plugins hook in at any phase; nothing runs outside the cycle. It scales from a single markdown blog to a multi-language, multi-format publishing platform with image / video / AI pipelines, live SSE-driven editors, semantic search, and typed frontend contracts and stays predictable in both directions.
9
+ Built for Node.js around a strict lifecycle and a composable plugin system. Every document, asset, and template flows through the same pipeline; plugins hook in at any phase. The same engine runs a single markdown blog and a multi-language publishing platform with PDF / email / AI-augmented asset pipelinessame lifecycle, more plugins.
10
10
 
11
11
  It's MIT-licensed, runs on Node 18+, has zero hosted dependencies, and the entire content tree it manages is a folder of `.md` and `.yml` files you can copy, diff, and version-control. **The portability promise is the architecture, not a feature.**
12
12
 
@@ -30,25 +30,27 @@ Build mikser into the parts of your application that are content-shaped. Keep th
30
30
 
31
31
  **Your content stays yours.** Source files live on disk as `.md`, `.yml`, `.html` with YAML front-matter. The build output is plain static files. No database lock-in, no proprietary export format. The whole content tree is copyable, diffable, and version-controllable with git — your site is portable on day one and on year ten.
32
32
 
33
+ **Static-first with a built-in live channel.** Most content engines pick one side: static-site generators (Hugo, Eleventy, Jekyll) are fast but rebuild-only; headless CMSes (Sanity, Contentful, Strapi) are live but every page is an API round-trip. Mikser composes both — content publishes as static files by default (fast first paint, no API on the happy path), and the live channel arrives on top, so edits show up in connected clients without a refresh and without losing the static advantage.
34
+
33
35
  **Incremental builds that scale.** Mikser tracks every entity in a journal. When a file changes, only the affected entities re-process — not the whole site graph. On 10k+ documents this dramatically outpaces tools that rebuild more on every change.
34
36
 
35
37
  **Concurrent rendering.** Renders fan out across a worker pool that keeps every CPU core hot. Multi-format outputs (HTML, PDF, MJML email, etc.) generate in parallel from the same source.
36
38
 
37
- **Image, video, and AI pipelines authored as pluginsnot configured.** The `assets` plugin runs user-written preset modules over binary inputs. A preset is a plain Node module: ~10 lines around `sharp` resize an image, ~10 lines around `fluent-ffmpeg` transcode a video, ~30 lines around the Replicate API run an AI upscaler. The pipeline isn't a fixed menu of operations — it's whatever Node can call. Most SSGs cap your asset processing at "resize and convert format." Mikser caps it at "what can Node do." Compose with the `resources` plugin and your DAM, CDN, or company content server becomes an upstream input — uploaded files end up transcoded, watermarked, AI-enhanced, and deployed without manual handoff. See [the assets / resources docs](./documentation/plugins.md#assets) for end-to-end examples.
39
+ **Asset pipelines are whatever Node can do.** Most static frameworks (Astro, Next.js, Hugo) ship image optimization and stop there video transcoding, AI upscaling, watermarking all need a separate service. Mikser runs user-written modules over binary inputs: ~10 lines around `sharp` resize an image, ~10 around `fluent-ffmpeg` transcode a video, ~30 around the Replicate API upscale with AI. Anything an npm package can do, your pipeline can do including pulling uploads from a DAM or CDN through the same flow.
38
40
 
39
41
  **One lifecycle, everything composes.** Plugins hook into 20+ named lifecycle phases. A search-indexing plugin shares the same journal iteration as an email-rendering plugin and a PDF-postprocessing plugin — no glue code, no orchestration layer. The engine doesn't know which plugins are loaded; plugins don't have to know about each other.
40
42
 
41
43
  **Run anywhere.** The same CLI handles one-shot builds, watch-mode dev loops, and a long-running HTTP server with a shared Express app. `npx mikser` ships a static site; `mikser --watch` is the dev loop; `mikser --server` exposes a live admin/API.
42
44
 
43
- **Survives backend outages.** The `api` plugin's [per-query disk cache](./documentation/caching.md) writes every cacheable list response to `out/` keyed by the request URL. A stock-nginx reverse proxy can fail over to the cached file when mikser is unreachable same URL, transparent to the client, no Lua or extra modules required. Production frontends keep rendering routes during deploys, brief outages, and the upstream-blip-of-the-week. Opt in per endpoint with `cache: true`; see [Caching and reverse-proxy failover](./documentation/caching.md) for the working config.
45
+ **Outages don't take you down.** Headless CMSes (Contentful, Sanity, Strapi) treat the API as the source of truth when it blinks, every frontend errors out. Mikser inverts that: reads become static files on disk, the live channel layers on top. A reverse proxy keeps serving the files when mikser blips. Visitors don't notice; live updates pause until mikser returns.
44
46
 
45
- **Library mode.** Mikser is also a library. `useRenderer`, `useCollection`, `findSimilar`, and direct lifecycle hooks let you embed the engine inside an existing Node app instead of running it as a CLI.
47
+ **Library mode.** Mikser is also a library. `useRenderer`, `useCollection`, and direct lifecycle hooks let you embed the engine inside an existing Node app instead of running it as a CLI — plugins like `vector` add their own primitives the same way.
46
48
 
47
49
  **Open source.** MIT-licensed, on GitHub, no telemetry, no auth wall, no SaaS dependency. What you see is what runs.
48
50
 
49
51
  ## Built for AI-assisted development
50
52
 
51
- Files-as-source isn't just a portability story — it makes the project unusually friendly to AI coding agents. Most setups lose time configuring an agent's access to the data: tokens, schemas, MCP servers, sandboxed query environments. Mikser sidesteps all of it because the content is text files the agent can already read with the tools it already has.
53
+ Files-as-source isn't just a portability story — it makes the project unusually friendly to AI coding agents. There's a static-time half (the agent reads your tree the way it reads any repo — no DB connection, no schema upload, no sandboxed query layer to learn) and a runtime half (when the agent needs to write or render, mikser ships its own MCP server in core, so it talks to the live engine instead of a parallel REST shim you have to maintain).
52
54
 
53
55
  **Zero infra friction for discovery.** An agent can `rg "type: product"` across the content tree to find every product doc in a second. No DB connection, no API token, no schema file to parse.
54
56
 
@@ -60,10 +62,39 @@ Files-as-source isn't just a portability story — it makes the project unusuall
60
62
 
61
63
  **Plugin-by-example.** Authoring a new plugin? There are 15+ existing ones in the same shape to pattern-match against. Convention is dense enough that new plugins look like the old ones without coaching.
62
64
 
63
- **One-shot bootstrap via Claude Code.** The [`mikser-io-claude-plugin`](https://github.com/almero-digital-marketing/mikser-io-claude-plugin) wraps the setup story above into a single skill. Register the repo as a Claude Code marketplace and install the plugin (`/plugin marketplace add almero-digital-marketing/mikser-io-claude-plugin` then `/plugin install mikser-io-claude-plugin@mikser-io`), then in any Vue 3, React, or SvelteKit project — or in a blank directory — say "add mikser to this app." It detects the framework (or scaffolds a fresh starter via `create-vite` / `sv create`), wires the matching framework SDK, composes with your existing router rather than replacing it, and optionally lays down a `mikser-content/` sibling folder with Zod schemas and starter documents so the backend works on first run.
65
+ **One-shot bootstrap via Claude Code.** The [`mikser-io-claude-plugin`](https://github.com/almero-digital-marketing/mikser-io-claude-plugin) wraps the whole setup into a single skill. Register the marketplace, install once:
66
+
67
+ ```
68
+ /plugin marketplace add almero-digital-marketing/mikser-io-claude-plugin
69
+ /plugin install mikser-io-claude-plugin@mikser-io
70
+ ```
71
+
72
+ …then in any Vue 3, React, or SvelteKit project — or in a blank directory — say *"add mikser to this app."* It detects the framework (or scaffolds a fresh starter via `create-vite` / `sv create`), wires the matching framework SDK without replacing your router, and optionally drops a `mikser-content/` sibling folder with Zod schemas and starter documents so the backend works on first run.
73
+
74
+ The runtime half — the agent driving the live engine, not just reading the tree — gets its own section below.
64
75
 
65
76
  The honest caveat: this advantage is real on **content-shaped work** — adding pages, restructuring collections, generating new layouts, building frontends. It doesn't make mikser better for non-content tasks (concurrency bugs in the worker pool, database tuning elsewhere in your stack); those are plain Node debugging like anywhere else. The visibility advantage also degrades past ~10k documents — at that scale the agent queries via the SDK instead of grepping the tree, which is still good but less "see everything at once."
66
77
 
78
+ ## Control mikser from your AI agent
79
+
80
+ Add `--mcp` to your mikser command and any MCP-speaking client — Claude Desktop, Claude Code, ChatGPT, custom agents — connects to the running engine. From inside a chat, your AI can:
81
+
82
+ - read every entity in the catalog
83
+ - write new content files (markdown, layouts, configuration) — writes land on disk and the next cycle picks them up
84
+ - render any layout for preview without touching the output folder
85
+ - watch every build log as it streams past
86
+ - introspect engine state — current lifecycle phase, effective config, recent log buffer
87
+
88
+ Plugins extend the tool surface the same way they mount HTTP routes; install the plugin, the agent gets new verbs. No glue code, no per-project agent wiring.
89
+
90
+ ```bash
91
+ mikser --server --mcp # mounts MCP at /mcp on the same port as --server
92
+ ```
93
+
94
+ What that feels like in practice: *"draft three hero-section variants and show me previews"* — three layouts written, three previews returned inline, one chat turn. *"Why did the build break?"* — the agent reads the rolling log buffer and answers from the same view your terminal sees. Operator, AI, and any observer dashboard share the same engine because mikser is single-tenant by design.
95
+
96
+ Full tool reference and twelve worked scenarios in [MCP — talking to mikser from AI](./documentation/mcp.md).
97
+
67
98
  ## Plugins on top of the engine
68
99
 
69
100
  The engine is what stays stable — the lifecycle, the catalog, the file-based content model. Plugins are independent npm packages sitting on the plugin API: some are essential to the SSG workflow, some give external systems HTTP access to the catalog, some are integrations that earn their keep on real projects, and some are probes that test how far the lifecycle stretches without touching the core. Install what a project needs; drop what it doesn't.
@@ -92,7 +123,10 @@ The engine is what stays stable — the lifecycle, the catalog, the file-based c
92
123
  |---|---|
93
124
  | [`mikser-io-vector`](https://github.com/almero-digital-marketing/mikser-io-vector) | OpenAI embeddings + semantic search (sqlite-vec or pgvector) |
94
125
  | [`mikser-io-plugin-schemas`](https://github.com/almero-digital-marketing/mikser-io-plugin-schemas) | Zod-backed entity validation + auto-generated TypeScript declarations for the SDK |
95
- | `archive`, `mapper`, `live`, `aml` | Specialty integrations |
126
+ | [`mikser-io-archive`](https://github.com/almero-digital-marketing/mikser-io-archive) | Persist matching entities to YAML — audit trail, versioned content history, downstream export |
127
+ | `mapper` | Run config-supplied transforms over matched entities each cycle (in-core, generic transformation layer) |
128
+ | [`mikser-io-live`](https://github.com/almero-digital-marketing/mikser-io-live) | Lightweight dev server with browser auto-refresh — pair with `--watch` for the classic save→reload loop |
129
+ | [`mikser-io-aml`](https://github.com/almero-digital-marketing/mikser-io-aml) | Parse [ArchieML](https://archieml.org/) (the NYT/ProPublica format) into `entity.meta` for non-technical authors |
96
130
 
97
131
  **Integration probes** — wrap a substantial external project as a plugin to confirm the lifecycle is open enough to host it without core changes. Treat these as feasibility evidence, not as a statement about where mikser is heading:
98
132
 
@@ -127,22 +161,14 @@ Each SDK ships TypeScript declarations so client projects get autocomplete on fi
127
161
  npm install mikser-io
128
162
  ```
129
163
 
130
- ```js
131
- // mikser.config.js
132
- export default {
133
- plugins: ['documents', 'layouts'],
134
- layouts: {
135
- cleanUrls: true,
136
- },
137
- }
138
- ```
139
-
140
164
  ```bash
141
165
  npx mikser # one-shot build
142
166
  npx mikser --watch # incremental dev loop
143
167
  npx mikser --server # build + serve at :3001
144
168
  ```
145
169
 
170
+ For a working starter — config with a real plugin set, sample `documents/`, expected output — see [Getting Started](./documentation/getting-started.md). Or skip straight to "add mikser to this app" via the [Claude Code plugin](#built-for-ai-assisted-development) above.
171
+
146
172
  ## Core Concepts
147
173
 
148
174
  - **Lifecycle** — Processing runs through fixed phases: initialize → load → import → process → persist → render → finalize. Plugins hook into any phase.
@@ -166,12 +192,18 @@ The shape mikser **doesn't** fit cleanly: anything with non-technical content au
166
192
 
167
193
  ## Engineering discipline
168
194
 
169
- A few things this project takes seriously:
195
+ What you get from how this project is built:
196
+
197
+ - **Every load-bearing decision has an ADR.** The [`decisions/`](./documentation/decisions/) folder names which choices are structural — files-as-source, journal+catalog split, plugin-as-factory, when something goes in core vs. ships as a plugin — and explains what protects them. When you push against one, there's a written answer waiting instead of folklore.
198
+ - **Engine stays small; capability ships in plugins.** The 15+ plugin ecosystem adds features without core changes, so your upgrade cost stays low. Probes like `decap` (a full third-party CMS mounted in ~150 lines, zero engine changes) are deliberate evidence the extension model holds where it counts.
199
+ - **Builds are deterministic; no async middleware layer.** The journal is the only synchronization primitive — no event bus, no IoC container, no orchestrator running plugins in surprising order. The lifecycle is a list of named phases; "what ran when?" has an answer you can read off the source.
200
+ - **The whole engine is one read.** The [Architecture Overview](./documentation/overview.md) walks the full pipeline top to bottom. Onboarding a new engineer is an afternoon, not a tour through fifteen reference docs.
201
+
202
+ ## Acknowledgments
203
+
204
+ The earliest version of mikser was inspired by [DocPad](https://github.com/docpad/docpad) (Benjamin Lupton, with Michael Duane Mooring and Rob Loach). DocPad's "freeway, not a box" philosophy — files on disk, any pre-processor or template engine, plugin-by-convention extension — shaped how mikser started.
170
205
 
171
- - **ADRs for load-bearing decisions.** The [`decisions/`](./documentation/decisions/) folder names which choices are structural files-as-source, journal+catalog split, plugin-as-factory, compose-via-protocols and explains what protects them. Read those before proposing a feature that pushes against one.
172
- - **Engine stability, plugin churn.** The 15+ plugins in the ecosystem add capability without core changes. The integration probes (e.g. `decap`, mounting a third-party CMS in ~150 lines) are deliberate evidence that the extension model holds.
173
- - **Deterministic builds.** The journal is the synchronization primitive. There's no event-passing layer, no IoC container, no plugin orchestrator. The engine knows how to fire phases in order; everything else falls out of that.
174
- - **The mental model is one document.** The [Architecture Overview](./documentation/overview.md) is one read for the full top-to-bottom picture. The reference docs exist for lookup; the overview exists for comprehension.
206
+ Mikser itself has a previous chapter: the [legacy 7.x line](https://github.com/almero-digital-marketing/mikser) (last release 2022) introduced the real-time SSG model the current engine still carries forward. The redesign dropped MongoDB (the catalog lives in-process now, not in a database), modernized to Node 18+ ESM with a structured 20-phase lifecycle, added the live SSE channel that powers the framework SDKs, and replaced cluster-based rendering with an async worker pool. Same intent content as files, real-time previews, multi-format output at scale — clearer foundations.
175
207
 
176
208
  ## Documentation Index
177
209
 
@@ -185,6 +217,7 @@ A few things this project takes seriously:
185
217
  | [Entities](./documentation/entities.md) | Users & Developers | Entity model, operations, journal, catalog |
186
218
  | [Rendering](./documentation/rendering.md) | Users & Developers | Render pipeline, render plugins, render modes |
187
219
  | [Watch Mode](./documentation/watch-mode.md) | Users | File watching, scheduled tasks, incremental builds |
220
+ | [MCP](./documentation/mcp.md) | Users | The `--mcp` server — tool surface, `mikser://` resources, twelve worked AI-driven scenarios |
188
221
  | [Caching](./documentation/caching.md) | Users (production) | The `cache: true` disk cache + working nginx config for reverse-proxy failover |
189
222
  | [Architecture](./documentation/architecture.md) | Developers | Module-level reference — what's in each file |
190
223
  | [API Reference](./documentation/api-reference.md) | Developers | Complete public API reference |
package/index.js CHANGED
@@ -9,4 +9,6 @@ export * from './src/plugins.js'
9
9
  export * from './src/manager.js'
10
10
  export * from './src/tracking.js'
11
11
  export * from './src/engine.js'
12
- export * from './src/api.js'
12
+ export * from './src/api.js'
13
+ export * from './src/source.js'
14
+ export * from './src/mcp.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "6.27.0",
3
+ "version": "7.0.0",
4
4
  "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -21,6 +21,7 @@
21
21
  "license": "ISC",
22
22
  "dependencies": {
23
23
  "@budibase/handlebars-helpers": "^0.14.2",
24
+ "@modelcontextprotocol/sdk": "^1.29.0",
24
25
  "await-semaphore": "^0.1.3",
25
26
  "axios": "^1.16.0",
26
27
  "chokidar": "^5.0.0",
@@ -62,8 +63,10 @@
62
63
  "mikser-io-render-eta": "file:../mikser-io-render-eta",
63
64
  "mikser-io-render-liquid": "file:../mikser-io-render-liquid",
64
65
  "mikser-io-render-markdown": "file:../mikser-io-render-markdown",
66
+ "mikser-io-schemas": "file:../mikser-io-schemas",
65
67
  "mikser-io-vector": "file:../mikser-io-vector",
66
- "sharp": "^0.34.5"
68
+ "sharp": "^0.34.5",
69
+ "zod": "^4.4.3"
67
70
  },
68
71
  "directories": {
69
72
  "test": "test"
package/src/catalog.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import runtime from './runtime.js'
2
2
  import { useLogger } from './engine.js'
3
- import { onLoaded, onPersist, onFinalized } from './lifecycle.js'
3
+ import { onInitialized, onPersist, onFinalized } from './lifecycle.js'
4
4
  import { useJournal } from './journal.js'
5
5
  import { OPERATION } from './constants.js'
6
6
  import { Low } from 'lowdb'
@@ -10,7 +10,13 @@ import _ from 'lodash'
10
10
 
11
11
  let catalog
12
12
 
13
- onLoaded(async () => {
13
+ // Same reasoning as journal.js — initialize the catalog in
14
+ // onInitialized so every plugin hook from onLoad onwards can safely
15
+ // call findEntity / findEntities and write through the journal.
16
+ // catalog.js imports useJournal from journal.js, so journal.js's
17
+ // onInitialized registers first within the phase — its hook runs
18
+ // before this one.
19
+ onInitialized(async () => {
14
20
  const adapter = new JSONFile(path.join(runtime.options.runtimeFolder, `catalog.json`))
15
21
  catalog = new Low(adapter, {
16
22
  entities: [],
@@ -27,10 +33,21 @@ onPersist(async () => {
27
33
  logger.trace('Database %s %s: %s', entity.collection, operation, entity.id)
28
34
  catalog.data.entities.push(entity)
29
35
  break
30
- case OPERATION.UPDATE:
36
+ case OPERATION.UPDATE: {
31
37
  logger.trace('Database %s %s: %s', entity.collection, operation, entity.id)
32
- catalog.chain.get('entities').find({ id: entity.id }).assign(entity).value()
38
+ // Upsert semantics: if the entity doesn't already exist,
39
+ // treat UPDATE as CREATE. Plugins that "ensure an entity
40
+ // is in the catalog" can call runtime.update without a
41
+ // findEntity-then-branch dance. Previously a no-op when
42
+ // the id was new, which was a silent footgun.
43
+ const existing = catalog.chain.get('entities').find({ id: entity.id }).value()
44
+ if (existing) {
45
+ catalog.chain.get('entities').find({ id: entity.id }).assign(entity).value()
46
+ } else {
47
+ catalog.data.entities.push(entity)
48
+ }
33
49
  break
50
+ }
34
51
  case OPERATION.DELETE:
35
52
  logger.trace('Database %s %s: %s', entity.collection, operation, entity.id)
36
53
  catalog.chain.get('entities').remove({ id: entity.id }).value()
package/src/engine.js CHANGED
@@ -49,6 +49,7 @@ export async function setup(options) {
49
49
  .option('-s --server [port]', 'start an Express server on the given port (defaults to 3001)')
50
50
  .option('--cors [origin]', 'restrict server CORS to a specific origin (default *)')
51
51
  .option('--no-cors', 'disable server CORS headers')
52
+ .option('--mcp [path]', 'enable MCP server (mounts at <path>, default /mcp)')
52
53
 
53
54
  Object.assign(runtime.options, options || runtime.engine.commander.parse(process.argv).opts())
54
55
  runtime.options.info = true
@@ -62,23 +63,22 @@ export async function setup(options) {
62
63
  runtime.options.info = false
63
64
  }
64
65
  runtime.engine.logger.notice = runtime.engine.logger.info
65
- })
66
-
67
- onInitialized(async () => {
68
- const logger = useLogger()
69
66
 
67
+ // Resolve folders inside onInitialize so journal.js and
68
+ // catalog.js (which initialize in onInitialized) see absolute
69
+ // paths and a guaranteed-existing runtimeFolder. The split is
70
+ // deliberate: engine's onInitialize does setup that the rest of
71
+ // the engine infrastructure depends on; onInitialized does
72
+ // things plugins may need.
70
73
  runtime.options.workingFolder = path.resolve(runtime.options.workingFolder)
71
74
  process.chdir(runtime.options.workingFolder)
72
75
 
73
76
  runtime.options.runtimeFolder = path.join(runtime.options.workingFolder, runtime.options.runtimeFolder || 'runtime')
74
77
  runtime.options.outputFolder = path.join(runtime.options.workingFolder, runtime.options.outputFolder || 'out')
75
78
 
76
- logger.info('Working folder: %s', runtime.options.workingFolder)
77
- logger.info('Output folder: %s', runtime.options.outputFolder)
78
-
79
79
  if (runtime.options.clear) {
80
80
  try {
81
- logger.info('Clearing folders')
81
+ runtime.engine.logger.info('Clearing folders')
82
82
  await rm(runtime.options.outputFolder, { recursive: true })
83
83
  await rm(runtime.options.runtimeFolder, { recursive: true })
84
84
  } catch (err) {
@@ -87,6 +87,13 @@ export async function setup(options) {
87
87
  }
88
88
  }
89
89
  await mkdir(runtime.options.runtimeFolder, { recursive: true })
90
+ })
91
+
92
+ onInitialized(async () => {
93
+ const logger = useLogger()
94
+
95
+ logger.info('Working folder: %s', runtime.options.workingFolder)
96
+ logger.info('Output folder: %s', runtime.options.outputFolder)
90
97
 
91
98
  // Server bring-up: two paths, controlled by which of these are set.
92
99
  //
@@ -146,6 +153,35 @@ export async function setup(options) {
146
153
  logger.info('CORS enabled: %s', origin)
147
154
  }
148
155
  }
156
+
157
+ // MCP substrate — same engine-provides-transport, plugins-
158
+ // register-tools shape as Express. See
159
+ // documentation/decisions/0006-when-to-add-to-core.md for
160
+ // the justification. The substrate object is exposed at
161
+ // runtime.options.mcp; plugins use it directly via the SDK
162
+ // (server.registerTool / server.registerResource). The
163
+ // transport is mounted later, after plugin routes have had
164
+ // a chance to register (see the onLoad below that handles
165
+ // static serving + listen).
166
+ if (runtime.options.mcp) {
167
+ try {
168
+ const { createMcpSubstrate, wireLoggerToMcp } = await import('./mcp.js')
169
+ runtime.options.mcpPath = typeof runtime.options.mcp === 'string'
170
+ ? runtime.options.mcp
171
+ : '/mcp'
172
+ runtime.options.mcp = createMcpSubstrate()
173
+
174
+ // Fan every engine log call out to MCP clients as
175
+ // `notifications/message`. Wraps in-place so the
176
+ // existing logger reference (held by plugins, render
177
+ // workers, useLogger consumers) gains the side-channel
178
+ // automatically — no second logger to thread through.
179
+ wireLoggerToMcp(runtime.engine.logger, runtime.options.mcp)
180
+ logger.info('MCP substrate ready (mounts at %s when server is up)', runtime.options.mcpPath)
181
+ } catch (err) {
182
+ logger.error('Failed to enable MCP: %s', err.message)
183
+ }
184
+ }
149
185
  })
150
186
 
151
187
  // Registered here (inside setup) so it runs AFTER plugins.js's onLoad
@@ -165,6 +201,15 @@ export async function setup(options) {
165
201
  const logger = useLogger()
166
202
  const { default: express } = await import('express')
167
203
 
204
+ // Mount MCP transport (if active) BEFORE the static
205
+ // catch-all so /mcp isn't swallowed. After this runs the
206
+ // server is fully reachable over HTTP for AI clients.
207
+ if (runtime.options.mcp && runtime.options.mcpPath) {
208
+ const { mountMcpOnExpress } = await import('./mcp.js')
209
+ await mountMcpOnExpress(runtime.options.app, runtime.options.mcp, runtime.options.mcpPath)
210
+ logger.info('MCP mounted: %s', runtime.options.mcpPath)
211
+ }
212
+
168
213
  // Serve the output folder as the catch-all static route.
169
214
  // Mounted LAST in the middleware chain so plugin routes
170
215
  // (e.g. /api/*) match first; anything that didn't match a
package/src/journal.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import runtime from './runtime.js'
2
- import { onLoaded, onCancelled, onFinalized } from './lifecycle.js'
2
+ import { onInitialized, onCancelled, onFinalized } from './lifecycle.js'
3
3
  import { unlink } from 'fs/promises'
4
4
  import knex from 'knex'
5
5
  import path from 'path'
@@ -81,7 +81,15 @@ export async function clearJournal(aborted) {
81
81
  }
82
82
  }
83
83
 
84
- onLoaded(async () => {
84
+ // Initialize the journal in onInitialized rather than onLoaded.
85
+ // The engine resolves runtime.options.runtimeFolder in its own
86
+ // onInitialized hook; ours runs after (engine.js is imported before
87
+ // any plugin loads, so its onInitialized registers first). This way
88
+ // every plugin hook from onLoad onwards — including plugin onLoaded
89
+ // — can safely call runtime.create / runtime.update without
90
+ // depending on the order in which journal.js's module registered
91
+ // relative to theirs.
92
+ onInitialized(async () => {
85
93
  const filename = path.join(runtime.options.runtimeFolder, `journal.db`)
86
94
  try {
87
95
  await unlink(filename)