mikser-io-mcp 1.22.0 → 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.
- package/index.js +66 -25
- package/package.json +2 -2
- 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
|
-
//
|
|
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
|
//
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
explain,
|
|
45
45
|
buildReport,
|
|
46
46
|
requestReport,
|
|
47
|
+
faults,
|
|
47
48
|
nextCycleId,
|
|
48
49
|
whenCycleCompletes,
|
|
49
50
|
cycleHistory,
|
|
@@ -62,6 +63,8 @@ import {
|
|
|
62
63
|
useProvenance,
|
|
63
64
|
sourcesBehind,
|
|
64
65
|
sourcesOf,
|
|
66
|
+
provideService,
|
|
67
|
+
useService,
|
|
65
68
|
registerTool as coreRegisterTool,
|
|
66
69
|
toolNames as coreToolNames,
|
|
67
70
|
toolSchema as coreToolSchema,
|
|
@@ -244,7 +247,7 @@ function wrapMutatingHandler(handler) {
|
|
|
244
247
|
// here so every mutating tool answers the same way, and only when the
|
|
245
248
|
// set actually recorded something: an id for a call that wrote
|
|
246
249
|
// nothing is a handle to nothing.
|
|
247
|
-
return attachChangeSet(result, id)
|
|
250
|
+
return await attachChangeSet(result, id)
|
|
248
251
|
}
|
|
249
252
|
}
|
|
250
253
|
|
|
@@ -258,15 +261,15 @@ function actingPrincipal() {
|
|
|
258
261
|
// everything else agree about who is calling.
|
|
259
262
|
const principal = authContext.getStore()?.principal
|
|
260
263
|
if (!principal) return 'agent'
|
|
261
|
-
const role = actingRole(principal.roles ?? [],
|
|
264
|
+
const role = actingRole(principal.roles ?? [], useService('roles')?.catalogue ?? {})
|
|
262
265
|
const subject = principal.subject ?? 'agent'
|
|
263
266
|
return role ? `${subject} (${role})` : subject
|
|
264
267
|
}
|
|
265
268
|
|
|
266
269
|
// Fold the id into an MCP result's JSON text block, leaving anything else —
|
|
267
270
|
// an image, an error, a non-JSON body — exactly as the tool produced it.
|
|
268
|
-
function attachChangeSet(result, id) {
|
|
269
|
-
if (!findChangeSet(id)) return result
|
|
271
|
+
async function attachChangeSet(result, id) {
|
|
272
|
+
if (!await findChangeSet(id)) return result
|
|
270
273
|
const content = result?.content
|
|
271
274
|
if (result?.isError || !Array.isArray(content)) return result
|
|
272
275
|
return {
|
|
@@ -332,11 +335,24 @@ export function createMcpSubstrate() {
|
|
|
332
335
|
const schema = coreToolSchema(name)
|
|
333
336
|
if (!schema) continue
|
|
334
337
|
try {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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)
|
|
340
356
|
bound.tools++
|
|
341
357
|
} catch (err) {
|
|
342
358
|
runtime.engine?.logger?.debug(
|
|
@@ -735,7 +751,7 @@ function stalePackages(workingFolder) {
|
|
|
735
751
|
substrate.registerTool(
|
|
736
752
|
'mikser_ping',
|
|
737
753
|
{
|
|
738
|
-
description: 'Return mikser engine identity, current lifecycle phase, and (if --server is on) where the HTTP server is reachable. Use to confirm the connection is live before issuing other tool calls and to learn the base URL for preview outputs.\n\n`plugins` lists what this mikser is built from — every installed mikser package with its purpose, version and links, and `active: true` on the ones actually running. Read it once to know what the system can do before reasoning about what it should.\n\nCheck `stale` before trusting any other tool, and before reporting a bug: it lists mikser packages installed SINCE this process booted, whose code is therefore not the code answering you. A running process never re-reads node_modules, and --watch does not change that — it reloads content, not dependencies. When `stale` is non-empty the fix is a restart, not a bug report.\n\nThe `auth` block names the ROLE this session acts as, what it may write and what it may only read — and `auth.roles` lists EVERY role on this site with the same reach, the acting one marked. That is INFORMATIONAL: report what you cannot do and stop. There is no way to request or change a role and none will be added — the listing names a person to ask, not a privilege to obtain.',
|
|
754
|
+
description: 'Return mikser engine identity, current lifecycle phase, and (if --server is on) where the HTTP server is reachable. Use to confirm the connection is live before issuing other tool calls and to learn the base URL for preview outputs.\n\n`plugins` lists what this mikser is built from — every installed mikser package with its purpose, version and links, and `active: true` on the ones actually running. Read it once to know what the system can do before reasoning about what it should.\n\nCheck `faults` before reading any empty result as a fact about the site: it is absent unless a subsystem has reported that it cannot work, and while it is present a tool may be answering emptily because it is unable to answer rather than because there is nothing to say. Each entry names the condition, when it was first and last seen, and how often — report it and stop rather than working around it.\n\nCheck `stale` before trusting any other tool, and before reporting a bug: it lists mikser packages installed SINCE this process booted, whose code is therefore not the code answering you. A running process never re-reads node_modules, and --watch does not change that — it reloads content, not dependencies. When `stale` is non-empty the fix is a restart, not a bug report.\n\nThe `auth` block names the ROLE this session acts as, what it may write and what it may only read — and `auth.roles` lists EVERY role on this site with the same reach, the acting one marked. That is INFORMATIONAL: report what you cannot do and stop. There is no way to request or change a role and none will be added — the listing names a person to ask, not a privilege to obtain.',
|
|
739
755
|
inputSchema: {},
|
|
740
756
|
},
|
|
741
757
|
async () => ({
|
|
@@ -752,6 +768,13 @@ function stalePackages(workingFolder) {
|
|
|
752
768
|
// Empty is the normal case and means what it says: the code
|
|
753
769
|
// answering you is the code on disk.
|
|
754
770
|
...stalePackages(runtime.options.workingFolder),
|
|
771
|
+
// Subsystems that have reported themselves unable to
|
|
772
|
+
// work. Absent is the normal case, and the reason this is
|
|
773
|
+
// here at all: a tool answering [] because it is BROKEN
|
|
774
|
+
// and one answering [] because nothing matched are the
|
|
775
|
+
// same answer, and the log line that said which is a
|
|
776
|
+
// channel an agent never reads.
|
|
777
|
+
...faultStatus(),
|
|
755
778
|
// What this mikser is made of. An agent can otherwise see
|
|
756
779
|
// what it may write and nothing about the machine doing
|
|
757
780
|
// the writing — and a capability like `drive:layouts`
|
|
@@ -820,8 +843,8 @@ function authStatus() {
|
|
|
820
843
|
const authority = describeAuthority({
|
|
821
844
|
capabilities: principal.capabilities,
|
|
822
845
|
roles: principal.roles ?? [],
|
|
823
|
-
catalogue:
|
|
824
|
-
summaries:
|
|
846
|
+
catalogue: useService('roles')?.catalogue ?? {},
|
|
847
|
+
summaries: useService('roles')?.summaries ?? {},
|
|
825
848
|
})
|
|
826
849
|
const exp = principal.claims?.exp
|
|
827
850
|
if (!exp) {
|
|
@@ -1047,6 +1070,17 @@ async function readIfText(uri) {
|
|
|
1047
1070
|
// Kept as a function rather than a const so each call re-reads
|
|
1048
1071
|
// runtime.options — covers the case where --server flips on after
|
|
1049
1072
|
// the substrate was created (rare but possible programmatically).
|
|
1073
|
+
// Subsystems that have reported themselves unable to work, if any.
|
|
1074
|
+
//
|
|
1075
|
+
// Omitted entirely when there are none, so presence is meaningful and a
|
|
1076
|
+
// healthy ping stays the short document it was. `last` is the field that says
|
|
1077
|
+
// whether a fault is still live: one seen only at boot and one firing every
|
|
1078
|
+
// cycle read the same by presence alone.
|
|
1079
|
+
function faultStatus() {
|
|
1080
|
+
const open = faults()
|
|
1081
|
+
return open.length ? { faults: open } : {}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1050
1084
|
function serverInfo() {
|
|
1051
1085
|
const opts = runtime.options
|
|
1052
1086
|
const hasInternalServer = opts.server != null && opts.port != null
|
|
@@ -1478,7 +1512,7 @@ function pinoLevelNumber(name) {
|
|
|
1478
1512
|
//
|
|
1479
1513
|
// The factory creates the substrate SYNCHRONOUSLY so other plugins
|
|
1480
1514
|
// listed AFTER 'mcp' in the array can register tools/resources at
|
|
1481
|
-
// their own onLoaded hook
|
|
1515
|
+
// their own onLoaded hook by asking core for the 'mcp' service
|
|
1482
1516
|
// gating already in place from the in-core era. The plugin MUST be
|
|
1483
1517
|
// FIRST in the user's plugins array — that's a documented invariant
|
|
1484
1518
|
// in this repo's README.
|
|
@@ -1520,12 +1554,19 @@ export function mcp(options = {}) {
|
|
|
1520
1554
|
add(runtime.options.corsExposeHeaders, ['mcp-session-id', 'mcp-protocol-version'])
|
|
1521
1555
|
}
|
|
1522
1556
|
|
|
1523
|
-
//
|
|
1524
|
-
//
|
|
1525
|
-
//
|
|
1526
|
-
//
|
|
1527
|
-
//
|
|
1528
|
-
|
|
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' })
|
|
1529
1570
|
// mikser_build_report needs the cycle recorded, and recording is off
|
|
1530
1571
|
// unless a reader asks for it — see report.js requestReport.
|
|
1531
1572
|
requestReport()
|
|
@@ -1546,7 +1587,7 @@ export function mcp(options = {}) {
|
|
|
1546
1587
|
// reference (held by plugins, render workers, useLogger
|
|
1547
1588
|
// consumers) gains the side-channel automatically.
|
|
1548
1589
|
if (runtime.engine?.logger) {
|
|
1549
|
-
wireLoggerToMcp(runtime.engine.logger,
|
|
1590
|
+
wireLoggerToMcp(runtime.engine.logger, substrate)
|
|
1550
1591
|
logger.debug('MCP logger wiring active')
|
|
1551
1592
|
}
|
|
1552
1593
|
|
|
@@ -1556,7 +1597,7 @@ export function mcp(options = {}) {
|
|
|
1556
1597
|
if (runtime.options.app && runtime.options.mcpPath) {
|
|
1557
1598
|
await mountMcpOnExpress(
|
|
1558
1599
|
runtime.options.app,
|
|
1559
|
-
|
|
1600
|
+
substrate,
|
|
1560
1601
|
runtime.options.mcpPath,
|
|
1561
1602
|
)
|
|
1562
1603
|
}
|
|
@@ -1571,7 +1612,7 @@ export function mcp(options = {}) {
|
|
|
1571
1612
|
// are silently skipped.
|
|
1572
1613
|
onLoaded(() => {
|
|
1573
1614
|
const logger = useLogger()
|
|
1574
|
-
const mcp =
|
|
1615
|
+
const mcp = substrate
|
|
1575
1616
|
if (!runtime.refs) {
|
|
1576
1617
|
logger.debug('mikser_refs_* tools skipped: runtime.refs not initialized')
|
|
1577
1618
|
return
|
|
@@ -1976,7 +2017,7 @@ export function mcp(options = {}) {
|
|
|
1976
2017
|
})
|
|
1977
2018
|
|
|
1978
2019
|
// mikser_layouts_inspect moved out — registered by mikser-io-layouts
|
|
1979
|
-
// directly against
|
|
2020
|
+
// directly against the substrate, same pattern vector and
|
|
1980
2021
|
// schemas use. Domain-owned tools live with the domain plugin; mcp
|
|
1981
2022
|
// is registry + transport, not a tool catalog.
|
|
1982
2023
|
|
|
@@ -1998,7 +2039,7 @@ export function mcp(options = {}) {
|
|
|
1998
2039
|
//
|
|
1999
2040
|
onLoaded(() => {
|
|
2000
2041
|
const logger = useLogger()
|
|
2001
|
-
const mcp =
|
|
2042
|
+
const mcp = substrate
|
|
2002
2043
|
|
|
2003
2044
|
const { render: mcpRender } = useRenderer(runtime, {
|
|
2004
2045
|
defaultTimeout: options.renderTimeout ?? 30_000,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikser-io-mcp",
|
|
3
|
-
"version": "
|
|
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": "^
|
|
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
|
-
// (
|
|
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
|
-
//
|
|
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
|
-
|
|
139
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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()
|