mikser-io-mcp 1.23.1 → 2.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.
Files changed (3) hide show
  1. package/index.js +43 -21
  2. package/package.json +2 -2
  3. package/preview.js +10 -8
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // MCP substrate for mikser-io. The mcp plugin exposes
2
- // `runtime.options.mcp` (a substrate object) at factory time so other
2
+ // the 'mcp' service (a substrate object) at factory time so other
3
3
  // plugins can register tools / resources / prompts against it at their
4
4
  // onLoaded hook with the same shape as the SDK's McpServer.
5
5
  //
@@ -63,6 +63,8 @@ import {
63
63
  useProvenance,
64
64
  sourcesBehind,
65
65
  sourcesOf,
66
+ provideService,
67
+ useService,
66
68
  registerTool as coreRegisterTool,
67
69
  toolNames as coreToolNames,
68
70
  toolSchema as coreToolSchema,
@@ -259,7 +261,7 @@ function actingPrincipal() {
259
261
  // everything else agree about who is calling.
260
262
  const principal = authContext.getStore()?.principal
261
263
  if (!principal) return 'agent'
262
- const role = actingRole(principal.roles ?? [], runtime.options.roles?.catalogue ?? {})
264
+ const role = actingRole(principal.roles ?? [], useService('roles')?.catalogue ?? {})
263
265
  const subject = principal.subject ?? 'agent'
264
266
  return role ? `${subject} (${role})` : subject
265
267
  }
@@ -333,11 +335,24 @@ export function createMcpSubstrate() {
333
335
  const schema = coreToolSchema(name)
334
336
  if (!schema) continue
335
337
  try {
336
- server.registerTool(
337
- exposed,
338
- { description: schema.description, inputSchema: zodShapeFrom(schema.inputSchema) },
339
- (args) => coreInvokeTool(name, args),
340
- )
338
+ // normalizeInputSchema, not zodShapeFrom: a plugin registering
339
+ // against the engine may describe its parameters EITHER in the
340
+ // engine's neutral vocabulary or in zod. zodShapeFrom assumes
341
+ // neutral, so a real zod shape came through it as a bag of
342
+ // optional strings — every constraint dropped, no error, and a
343
+ // tool that looked registered but refused nothing.
344
+ let inputSchema = normalizeInputSchema(schema.inputSchema)
345
+ let handler = (args) => coreInvokeTool(name, args)
346
+ // `mutates: true` reaches here now that the engine registry
347
+ // carries the whole definition. Without this a mutating tool
348
+ // registered against the engine would take no changeSet, and
349
+ // its writes would be unattributable — the one failure nothing
350
+ // downstream can repair.
351
+ if (schema.mutates) {
352
+ inputSchema = withChangeSetParams(inputSchema)
353
+ handler = wrapMutatingHandler(handler)
354
+ }
355
+ server.registerTool(exposed, { description: schema.description, inputSchema }, handler)
341
356
  bound.tools++
342
357
  } catch (err) {
343
358
  runtime.engine?.logger?.debug(
@@ -828,8 +843,8 @@ function authStatus() {
828
843
  const authority = describeAuthority({
829
844
  capabilities: principal.capabilities,
830
845
  roles: principal.roles ?? [],
831
- catalogue: runtime.options.roles?.catalogue ?? {},
832
- summaries: runtime.options.roles?.summaries ?? {},
846
+ catalogue: useService('roles')?.catalogue ?? {},
847
+ summaries: useService('roles')?.summaries ?? {},
833
848
  })
834
849
  const exp = principal.claims?.exp
835
850
  if (!exp) {
@@ -1497,7 +1512,7 @@ function pinoLevelNumber(name) {
1497
1512
  //
1498
1513
  // The factory creates the substrate SYNCHRONOUSLY so other plugins
1499
1514
  // listed AFTER 'mcp' in the array can register tools/resources at
1500
- // their own onLoaded hook with `if (!runtime.options.mcp) return`
1515
+ // their own onLoaded hook by asking core for the 'mcp' service
1501
1516
  // gating already in place from the in-core era. The plugin MUST be
1502
1517
  // FIRST in the user's plugins array — that's a documented invariant
1503
1518
  // in this repo's README.
@@ -1539,12 +1554,19 @@ export function mcp(options = {}) {
1539
1554
  add(runtime.options.corsExposeHeaders, ['mcp-session-id', 'mcp-protocol-version'])
1540
1555
  }
1541
1556
 
1542
- // Create substrate synchronously. Other plugins' factories /
1543
- // onLoaded hooks check `if (!runtime.options.mcp) return` before
1544
- // calling `mcp.simpleTool(...)` — so the substrate has to be on
1545
- // runtime.options.mcp by the time those run. The mcp plugin MUST
1546
- // be FIRST in the user's plugins array for that contract to hold.
1547
- runtime.options.mcp = createMcpSubstrate()
1557
+ // Offered as a service rather than assigned to runtime.options.mcp.
1558
+ //
1559
+ // Tools do not need this at all any more — a plugin registers those
1560
+ // against the engine's registry and this package binds them into each
1561
+ // session. What is left is genuinely MCP's: resources and prompts, which
1562
+ // core deliberately has no vocabulary for.
1563
+ //
1564
+ // A consumer asks core for it from a hook, by which time every factory
1565
+ // has run — which retires the rule this comment used to carry, that the
1566
+ // mcp plugin MUST be FIRST in the user's plugins array. Nothing enforced
1567
+ // that; put it second and the tools silently vanished.
1568
+ const substrate = createMcpSubstrate()
1569
+ provideService('mcp', substrate, { plugin: 'mikser-io-mcp' })
1548
1570
  // mikser_build_report needs the cycle recorded, and recording is off
1549
1571
  // unless a reader asks for it — see report.js requestReport.
1550
1572
  requestReport()
@@ -1565,7 +1587,7 @@ export function mcp(options = {}) {
1565
1587
  // reference (held by plugins, render workers, useLogger
1566
1588
  // consumers) gains the side-channel automatically.
1567
1589
  if (runtime.engine?.logger) {
1568
- wireLoggerToMcp(runtime.engine.logger, runtime.options.mcp)
1590
+ wireLoggerToMcp(runtime.engine.logger, substrate)
1569
1591
  logger.debug('MCP logger wiring active')
1570
1592
  }
1571
1593
 
@@ -1575,7 +1597,7 @@ export function mcp(options = {}) {
1575
1597
  if (runtime.options.app && runtime.options.mcpPath) {
1576
1598
  await mountMcpOnExpress(
1577
1599
  runtime.options.app,
1578
- runtime.options.mcp,
1600
+ substrate,
1579
1601
  runtime.options.mcpPath,
1580
1602
  )
1581
1603
  }
@@ -1590,7 +1612,7 @@ export function mcp(options = {}) {
1590
1612
  // are silently skipped.
1591
1613
  onLoaded(() => {
1592
1614
  const logger = useLogger()
1593
- const mcp = runtime.options.mcp
1615
+ const mcp = substrate
1594
1616
  if (!runtime.refs) {
1595
1617
  logger.debug('mikser_refs_* tools skipped: runtime.refs not initialized')
1596
1618
  return
@@ -1995,7 +2017,7 @@ export function mcp(options = {}) {
1995
2017
  })
1996
2018
 
1997
2019
  // mikser_layouts_inspect moved out — registered by mikser-io-layouts
1998
- // directly against `runtime.options.mcp`, same pattern vector and
2020
+ // directly against the substrate, same pattern vector and
1999
2021
  // schemas use. Domain-owned tools live with the domain plugin; mcp
2000
2022
  // is registry + transport, not a tool catalog.
2001
2023
 
@@ -2017,7 +2039,7 @@ export function mcp(options = {}) {
2017
2039
  //
2018
2040
  onLoaded(() => {
2019
2041
  const logger = useLogger()
2020
- const mcp = runtime.options.mcp
2042
+ const mcp = substrate
2021
2043
 
2022
2044
  const { render: mcpRender } = useRenderer(runtime, {
2023
2045
  defaultTimeout: options.renderTimeout ?? 30_000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io-mcp",
3
- "version": "1.23.1",
3
+ "version": "2.0.0",
4
4
  "description": "MCP (Model Context Protocol) substrate and tools for mikser-io. Extracted from core to iterate on its own release cadence.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -42,7 +42,7 @@
42
42
  "minimatch": "^10.0.3"
43
43
  },
44
44
  "peerDependencies": {
45
- "mikser-io": "^9.57.0",
45
+ "mikser-io": "^10.0.0",
46
46
  "zod": "^4.0.0"
47
47
  },
48
48
  "devDependencies": {
package/preview.js CHANGED
@@ -7,7 +7,7 @@
7
7
  // the in-memory preview cache.
8
8
  //
9
9
  // The cache itself lives in mikser-io's preview plugin
10
- // (runtime.options.preview.{store, get, stats, config}). This module
10
+ // (the `preview` service: {store, get, stats, config}). This module
11
11
  // reaches into it via that surface — no cross-plugin imports.
12
12
 
13
13
  import path from 'node:path'
@@ -15,7 +15,7 @@ import { readFileSync } from 'node:fs'
15
15
  import { fileURLToPath } from 'node:url'
16
16
  import { randomUUID, createHmac } from 'node:crypto'
17
17
  import { z } from 'zod'
18
- import { useRenderer, mimeForEntity, matchEntity } from 'mikser-io'
18
+ import { useRenderer, mimeForEntity, matchEntity, useService } from 'mikser-io'
19
19
 
20
20
  // Resolve a path relative to this module's file. Used to find the
21
21
  // public/ folder regardless of where the package is installed (works
@@ -122,7 +122,7 @@ const PREVIEW_UI_SHELL_HTML = readFileSync(path.join(__dirname, "public", "previ
122
122
  // Plugin function — invoked by mikser-io-mcp/index.js's factory after
123
123
  // the substrate is set up. Registers the MCP-UI surface (shell + modes
124
124
  // resource + preview_ui + ui_action) AND the preview-render tool (which
125
- // reaches into runtime.options.preview for the cache).
125
+ // asks core for the preview service).
126
126
  //
127
127
  // Not a default export plugin in the mikser sense — this is internal
128
128
  // composition. The mcp plugin's index.js is what mikser loads; this
@@ -135,15 +135,17 @@ export default ({
135
135
  findEntities,
136
136
  }) => {
137
137
  onLoaded(() => {
138
- if (!runtime.options.mcp) return
139
- const mcp = runtime.options.mcp
138
+ // Asked for from a hook, so every factory has run — including this
139
+ // package's own, which provides it.
140
+ const mcp = useService('mcp')
141
+ if (!mcp) return
140
142
  const { render: previewRender } = useRenderer(runtime, {
141
143
  defaultTimeout: runtime.config.preview?.renderTimeout ?? 30_000,
142
144
  })
143
145
 
144
146
  // mikser_preview_render — render an entity through the pipeline,
145
147
  // stash the bytes in the in-memory preview cache (provided by
146
- // mikser-io core's preview plugin via runtime.options.preview),
148
+ // mikser-io core's preview plugin, offered as the `preview` service),
147
149
  // and return a clickable URL. Requires --server (or any
148
150
  // engine-supplied Express app) so the URL is reachable.
149
151
  mcp.simpleTool(
@@ -155,7 +157,7 @@ export default ({
155
157
  },
156
158
  async ({ entity = {}, options = {} }) => {
157
159
  const logger = useLogger()
158
- const preview = runtime.options.preview
160
+ const preview = useService('preview')
159
161
  const ok = (data) => ({
160
162
  content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
161
163
  })
@@ -176,7 +178,7 @@ export default ({
176
178
  return fail('mikser_preview_render requires either --url <public-url> or --server to be set so the preview URL is reachable. Use mikser_render to get raw bytes inline instead.')
177
179
  }
178
180
  if (!preview) {
179
- return fail('mikser_preview_render requires the preview cache. Ensure mikser-io core is loaded and runtime.options.preview is available.')
181
+ return fail('mikser_preview_render requires the preview cache. Ensure the preview plugin from mikser-io core is in your plugins array.')
180
182
  }
181
183
 
182
184
  const cfg = preview.config()