@devframes/hub 0.9.8 → 0.9.10

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.
@@ -1,7 +1,7 @@
1
- import { i as InstallDevframeOptions, n as DevframeHubContext, t as CreateHubContextOptions } from "../context-BJFGlcf8.mjs";
2
- import { r as DEVFRAMES_HUB_BASE } from "../constants-DJrPlLi0.mjs";
1
+ import { i as InstallDevframeOptions, n as DevframeHubContext, t as CreateHubContextOptions } from "../context-Di-fhMKP.mjs";
2
+ import { r as DEVFRAMES_HUB_BASE } from "../constants-Bz4yclHo.mjs";
3
3
  import { DevframeInstanceRecord } from "devframe/internal";
4
- import { ConnectionMeta, DevframeDefinition, DevframeServiceInput, DevframeSseOptions, DevframeStorageScope, DevframeWsOptions, McpRouteOptions } from "devframe/types";
4
+ import { ConnectionMeta, DevframeDefinition, DevframeServiceInput, DevframeSseOptions, DevframeStorageScope, DevframeWsOptions, McpSetting } from "devframe/types";
5
5
  import { DevframeAuthHandler } from "devframe/node/auth";
6
6
  import { WsOriginRegistry } from "devframe/rpc/transports/ws-server";
7
7
  import { Buffer } from "node:buffer";
@@ -20,14 +20,14 @@ type Arrayable<T> = T | readonly T[];
20
20
  * One dock-type → prebuilt renderer-module registration for
21
21
  * {@link InitHubOptions.renderers}. The hub serves the module at
22
22
  * `<base>__renderers/<type>.mjs` and publishes it in the renderer manifest
23
- * (the `devframe:dock-renderers` shared-state slot), so any viewer — the
24
- * reference UI, a community viewer, a hand-rolled host page — lazily imports
23
+ * (the `devframe:dock-renderers` shared-state slot), so any viewer (the
24
+ * reference UI, a community viewer, a hand-rolled host page) lazily imports
25
25
  * it the first time a dock of that `type` needs rendering.
26
26
  *
27
27
  * The module must be a **self-contained browser ES module** (its framework
28
28
  * and styles bundled in) whose {@link DockRendererRegistration.importName}
29
29
  * export is a ready `DockRenderer`. Renderer packages ship a node helper
30
- * returning this shape — e.g. `jsonRenderUiRenderer()` from
30
+ * returning this shape, e.g. `jsonRenderUiRenderer()` from
31
31
  * `@devframes/json-render-ui/hub`.
32
32
  */
33
33
  interface DockRendererRegistration {
@@ -43,7 +43,7 @@ interface DockRendererRegistration {
43
43
  importName?: string;
44
44
  }
45
45
  /**
46
- * The UI slot of a hub instance — pure data, zero policy. The hub itself is
46
+ * The UI slot of a hub instance: pure data, zero policy. The hub itself is
47
47
  * headless: whoever fills this slot decides what a viewer looks like.
48
48
  * `@devframes/hub-ui` ships the reference implementation (`createUi()`);
49
49
  * Vite DevTools or any community viewer supplies its own object to the same
@@ -52,14 +52,14 @@ interface DockRendererRegistration {
52
52
  interface DevframeHubUi {
53
53
  /**
54
54
  * A standalone viewer SPA (built with relative asset paths) served at the
55
- * hub base itself — open `<base>` in a tab and the devtools are there.
55
+ * hub base itself; open `<base>` in a tab and the devtools are there.
56
56
  */
57
57
  viewer?: {
58
58
  /** Directory of the prebuilt viewer SPA. */
59
59
  distDir: string;
60
60
  };
61
61
  /**
62
- * A prebuilt, self-contained script served at `<base>embedded.js` — the
62
+ * A prebuilt, self-contained script served at `<base>embedded.js`: the
63
63
  * floating-devtools bootstrap a host page loads with one
64
64
  * `<script type="module" src="<base>embedded.js">` tag. Visibility policy
65
65
  * (always-on, keyboard-summoned, …) belongs entirely to this entry.
@@ -77,8 +77,8 @@ interface DevframeHubUi {
77
77
  */
78
78
  assets?: Record<string, () => string | Uint8Array>;
79
79
  /**
80
- * A setup hook run once during hub init with the hub context — the UI
81
- * slot's chance to publish its own static, boot-time config through the
80
+ * A setup hook run once during hub init with the hub context, giving the UI
81
+ * slot a chance to publish its own static, boot-time config through the
82
82
  * generic `ctx.staticConfig` (serialized into `ConnectionMeta.configs`).
83
83
  * The reference UI's `createUi()` uses it to set
84
84
  * `ctx.staticConfig.ui = { branding, … }`, reaching every mounted frame and
@@ -98,7 +98,7 @@ interface InitHubOptions {
98
98
  */
99
99
  version?: string;
100
100
  /**
101
- * Mount base the hub answers under — required so the mount path is
101
+ * Mount base the hub answers under, required so the mount path is
102
102
  * explicit at the call site (pass the exported {@link DEVFRAMES_HUB_BASE}
103
103
  * for the conventional `/__devframes/`). Every mounted devframe lives at
104
104
  * `<base><id>/`, so the host app needs exactly one catch-all route. The
@@ -118,19 +118,19 @@ interface InitHubOptions {
118
118
  * Host-level wire services to install, on top of whatever the mounted
119
119
  * devframes declare. Constructed (option sets merged) at the pre-setup
120
120
  * barrier, so every devframe's `setup` sees them ready. Reach for this to
121
- * configure a shared service centrally — e.g.
121
+ * configure a shared service centrally, e.g.
122
122
  * `services: [createShikiService({ themes })]`.
123
123
  */
124
124
  services?: DevframeServiceInput[];
125
125
  /**
126
126
  * Extra RPC declarations registered at context creation, alongside the
127
- * hub built-ins — forwarded to `createHubContext`'s
127
+ * hub built-ins, forwarded to `createHubContext`'s
128
128
  * `builtinRpcDeclarations`. Declarative mode only (a pre-built `context`
129
129
  * already made this choice).
130
130
  */
131
131
  rpcDeclarations?: CreateHubContextOptions['builtinRpcDeclarations'];
132
132
  /**
133
- * Bring your own hub context instead of `devframes` — for hosts that
133
+ * Bring your own hub context instead of `devframes`, for hosts that
134
134
  * assemble `createHubContext` + `ctx.install` themselves (with their own
135
135
  * `DevframeHost` serving the frames). The instance then serves only the
136
136
  * hub-level endpoints (`__connection.json`, `__index.json`,
@@ -139,14 +139,14 @@ interface InitHubOptions {
139
139
  */
140
140
  context?: DevframeHubContext;
141
141
  /**
142
- * Runs once the context exists and every `devframes` entry is mounted —
142
+ * Runs once the context exists and every `devframes` entry is mounted;
143
143
  * register docks, commands, terminals, and messages surfaces here.
144
144
  */
145
145
  configure?: (ctx: DevframeHubContext) => void | Promise<void>;
146
- /** See {@link DevframeHubUi} — omitted, the hub stays fully headless. */
146
+ /** See {@link DevframeHubUi}; omitted, the hub stays fully headless. */
147
147
  ui?: DevframeHubUi;
148
148
  /**
149
- * Prebuilt dock-renderer modules to serve and advertise — the composition
149
+ * Prebuilt dock-renderer modules to serve and advertise: the composition
150
150
  * seam that hands a renderer package (e.g. `@devframes/json-render-ui`) to
151
151
  * a prebuilt viewer. Each {@link DockRendererRegistration} is served at
152
152
  * `<base>__renderers/<type>.mjs` and published in the renderer manifest;
@@ -175,11 +175,11 @@ interface InitHubOptions {
175
175
  * > `ws.sidecar` (auto-port side-car) > the host driving upgrades itself,
176
176
  * while `url` overrides the advertisement (tunnel pattern) and `route`
177
177
  * renames the upgrade segment (default `__ws`). Pass `false` to serve no
178
- * WebSocket at all — clients connect over SSE instead (`backend: 'sse'`).
178
+ * WebSocket at all, so clients connect over SSE instead (`backend: 'sse'`).
179
179
  */
180
180
  ws?: DevframeWsOptions | false;
181
181
  /**
182
- * SSE RPC endpoint control, same contract as `initDevframe` — enabled by
182
+ * SSE RPC endpoint control, same contract as `initDevframe`, enabled by
183
183
  * default at `<base>__sse` as the more portable transport alongside the
184
184
  * WebSocket. Pass `false` to disable, or a {@link DevframeSseOptions} to
185
185
  * rename the route.
@@ -195,11 +195,18 @@ interface InitHubOptions {
195
195
  */
196
196
  auth?: boolean | DevframeAuthHandler;
197
197
  /**
198
- * Expose the **aggregate** MCP endpoint at `<base>__mcp` — one
198
+ * Expose the **aggregate** MCP endpoint at `<base>__mcp`: one
199
199
  * Streamable-HTTP server over the shared context's whole tool registry
200
- * (ids are already namespaced per plugin). Disabled by default.
200
+ * (ids are already namespaced per plugin). Defaults to `'auto'`: the route
201
+ * mounts once any mounted devframe (or an agent-flagged hub command)
202
+ * exposes an agent surface. `true` mounts it
203
+ * unconditionally with the loopback origin gate (trusting same-machine
204
+ * callers), an object opts into an {@link McpRouteOptions.authorization}
205
+ * identity check, `false` keeps it off. A mounted devframe's own `mcp`
206
+ * setting is ignored: the hub's aggregate route covers them all (`DF8005`
207
+ * warns when one asks for MCP while this is `false`).
201
208
  */
202
- mcp?: boolean | McpRouteOptions;
209
+ mcp?: McpSetting;
203
210
  /**
204
211
  * Public origin the host app is reachable at, or a getter. Derived lazily
205
212
  * from the first request when omitted.
@@ -217,14 +224,14 @@ interface InitHubOptions {
217
224
  register?: boolean | Partial<DevframeInstanceRecord>;
218
225
  /**
219
226
  * Advertise that this host runtime resolves **bare-specifier** client
220
- * scripts (`ClientScriptEntry.importFrom` naming an npm module) — a URL
227
+ * scripts (`ClientScriptEntry.importFrom` naming an npm module): a URL
221
228
  * template whose `{specifier}` token is replaced with the specifier,
222
229
  * published as `ConnectionMeta.configs.dock.clientModuleResolution` and
223
230
  * applied by every client-script loader. Declare it only when the host can
224
- * actually serve npm modules to the browser — a Vite host passes
231
+ * actually serve npm modules to the browser; a Vite host passes
225
232
  * `'/@id/{specifier}'` to route imports through its own module graph
226
233
  * (`@devframes/vite/hub` does). Left undeclared, registering a
227
- * bare-specifier client script warns `DF8111` — ship a self-contained
234
+ * bare-specifier client script warns `DF8111`; ship a self-contained
228
235
  * bundle and pass its URL instead.
229
236
  */
230
237
  clientModuleResolution?: string;
@@ -240,12 +247,12 @@ interface InitHubOptions {
240
247
  interface HubInstance {
241
248
  /**
242
249
  * The normalized mount base this hub answers under (leading and trailing
243
- * slash, e.g. `/__devframes/`). Reference it when wiring the mount — route
244
- * guards, middleware path checks — instead of repeating the string literal.
250
+ * slash, e.g. `/__devframes/`). Reference it when wiring the mount (route
251
+ * guards, middleware path checks) instead of repeating the string literal.
245
252
  */
246
253
  base: string;
247
254
  /**
248
- * Web-standard request handler for the whole hub — mount it on one
255
+ * Web-standard request handler for the whole hub; mount it on one
249
256
  * catch-all route under {@link HubInstance.base}.
250
257
  */
251
258
  handler: (request: Request) => Promise<Response>;
@@ -253,7 +260,7 @@ interface HubInstance {
253
260
  nodeMiddleware: (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void;
254
261
  /**
255
262
  * Route a host server's `upgrade` events to the shared RPC socket,
256
- * returning a detach function — the manual counterpart to the `server`
263
+ * returning a detach function, the manual counterpart to the `server`
257
264
  * option, for hosts that get their `node:http` server only after the hub
258
265
  * exists. Available on the default tier; a configured transport (`server`,
259
266
  * `ws.port`, `ws.sidecar`, `ws.url`) already owns the socket and reports
@@ -276,14 +283,14 @@ interface HubInstance {
276
283
  close: () => Promise<void>;
277
284
  }
278
285
  /**
279
- * Initiate a hub instance — the whole multi-devframe devtools surface
286
+ * Initiate a hub instance: the whole multi-devframe devtools surface
280
287
  * behind one framework-agnostic, web-standard handler. Every mounted
281
288
  * devframe shares one context (merged RPC registry, shared state, docks /
282
289
  * terminals / messages / commands), one WebSocket transport, and one Auth;
283
290
  * the instance serves each frame's SPA at `<base><id>/`, the discovery
284
291
  * endpoints (`__connection.json`, `__index.json`, `__client-imports.js`),
285
292
  * the aggregate MCP route, and whatever the {@link DevframeHubUi} slot
286
- * provides — the hub itself stays headless.
293
+ * provides, while the hub itself stays headless.
287
294
  *
288
295
  * The factory is synchronous and initializes eagerly, and binds no port of
289
296
  * its own: the WebSocket follows `server` / `ws` (see
@@ -1,19 +1,15 @@
1
- import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, r as DEVFRAMES_HUB_BASE } from "../constants-D1zicoJ4.mjs";
2
- import { a as resolveClientModuleSpecifier } from "../remote-url-Bgc7gtsP.mjs";
3
- import { m as prepareDevframe, t as createHubContext, y as diagnostics } from "../context-DYUkd3il.mjs";
4
- import { joinURL, withTrailingSlash, withoutLeadingSlash } from "ufo";
5
- import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_MCP_ROUTE, DEVFRAME_WS_ROUTE } from "devframe/constants";
6
- import { createH3DevframeHost, createInstanceShell, importRuntimeModule, resolveInstanceRegister } from "devframe/internal";
7
- import { resolve } from "pathe";
8
- import process from "node:process";
9
- import { existsSync } from "node:fs";
1
+ import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, r as DEVFRAMES_HUB_BASE } from "../constants-Da23JxCZ.mjs";
2
+ import { b as diagnostics, t as createHubContext } from "../context-BWaW6N53.mjs";
3
+ import { i as resolveRendererRegistrations, n as renderClientImportsModule, r as resolveDevframesInput, t as mountDevframes } from "../assemble-Hj36CCzH.mjs";
4
+ import { joinURL, withoutLeadingSlash } from "ufo";
5
+ import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_MCP_ROUTE } from "devframe/constants";
10
6
  import { readFile } from "node:fs/promises";
7
+ import process from "node:process";
8
+ import { createH3DevframeHost, createInstanceShell, importRuntimeModule, loadAutoMcpAdapter, resolveInstanceRegister, resolveMcpConfig } from "devframe/internal";
9
+ import { resolve } from "pathe";
11
10
  import { mountStaticHandler } from "devframe/utils/serve-static";
12
11
  import { H3 } from "h3";
13
12
  //#region src/node/initiate.ts
14
- function normalizeDevframeEntry(entry) {
15
- return "devframe" in entry ? entry : { devframe: entry };
16
- }
17
13
  /** Content-type for a UI asset key, inferred from its file extension. */
18
14
  function assetContentType(key) {
19
15
  if (key.endsWith(".json")) return "application/json; charset=utf-8";
@@ -23,81 +19,51 @@ function assetContentType(key) {
23
19
  if (key.endsWith(".html")) return "text/html; charset=utf-8";
24
20
  return "application/octet-stream";
25
21
  }
26
- /** Reserved filenames directly under the hub base — a frame id can't shadow them. */
27
- const RESERVED_HUB_PATHS = [
28
- DEVFRAME_CONNECTION_META_FILENAME,
29
- DEVFRAME_DOCK_IMPORTS_FILENAME,
30
- DEVFRAME_WS_ROUTE,
31
- DEVFRAME_MCP_ROUTE,
32
- "__index.json",
33
- "__renderers",
34
- "embedded.js"
35
- ];
36
22
  /**
37
- * Flatten the `devframes` input: await every thenable, call every factory,
38
- * spread the arrays, and drop the empty slots — so a host can build the list
39
- * conditionally (`isDev && loadInspect()`) without filtering it first.
23
+ * Resolve the shared hub context: reuse a caller-supplied one, or build an
24
+ * h3-backed context that serves the hub's connection meta under every frame.
40
25
  */
41
- function resolveDevframesInput(input) {
42
- return Promise.all(input.map(async (entry) => {
43
- const resolved = await (typeof entry === "function" ? entry() : entry);
44
- return (Array.isArray(resolved) ? resolved : [resolved]).filter((slot) => slot != null).map(normalizeDevframeEntry);
45
- })).then((arrays) => arrays.flat());
46
- }
47
- /**
48
- * Validate the renderer-module registrations fail-fast: route-safe types
49
- * (each becomes the `<base>__renderers/<type>.mjs` URL segment), one module
50
- * per type, and an existing bundle file (renderer packages are prebuilt).
51
- */
52
- function resolveRendererRegistrations(registrations) {
53
- const seen = /* @__PURE__ */ new Set();
54
- return registrations.map((registration) => {
55
- if (!/^[\w.-]+$/.test(registration.type)) throw diagnostics.DF8110({ type: registration.type });
56
- if (seen.has(registration.type)) throw diagnostics.DF8108({ type: registration.type });
57
- seen.add(registration.type);
58
- const file = resolve(registration.file);
59
- if (!existsSync(file)) throw diagnostics.DF8109({
60
- type: registration.type,
61
- file
62
- });
63
- return {
64
- ...registration,
65
- file
66
- };
26
+ async function resolveHubContext(options, app, cwd, api) {
27
+ if (options.context && options.devframes?.length) throw diagnostics.DF8002();
28
+ if (options.context) return options.context;
29
+ const host = {
30
+ ...createH3DevframeHost({
31
+ origin: () => api.origin() ?? "http://localhost",
32
+ appName: "devframes",
33
+ workspaceRoot: cwd,
34
+ mount: (mountBase, dir) => {
35
+ mountStaticHandler(app, mountBase, dir);
36
+ }
37
+ }),
38
+ ...options.getStorageDir ? { getStorageDir: options.getStorageDir } : {},
39
+ /**
40
+ * Serve the hub's own connection meta under every mounted frame's
41
+ * base, so each SPA discovers the shared RPC endpoint via its
42
+ * relative `./__connection.json` fetch; the meta's WS path is
43
+ * hub-base-absolute, so it resolves to the one shared socket no
44
+ * matter how deep the frame base is.
45
+ */
46
+ mountConnectionMeta: (frameBase) => {
47
+ app.use(joinURL(frameBase, DEVFRAME_CONNECTION_META_FILENAME), () => api.connectionMeta());
48
+ }
49
+ };
50
+ return createHubContext({
51
+ cwd,
52
+ workspaceRoot: cwd,
53
+ mode: "dev",
54
+ host,
55
+ ...options.rpcDeclarations ? { builtinRpcDeclarations: options.rpcDeclarations } : {}
67
56
  });
68
57
  }
69
58
  /**
70
- * Render the dock client-script import map as an ES module — one dynamic
71
- * import thunk per dock that carries a client script (`clientScript` on
72
- * iframe docks, `action`, `renderer`). External viewers import this module
73
- * from `<base>__client-imports.js` to load per-dock client code into the
74
- * host page; `importFrom` values are URL paths the host serves, or bare npm
75
- * specifiers resolved through the host's `clientModuleResolution` template.
76
- */
77
- function renderClientImportsModule(ctx) {
78
- const template = ctx.staticConfig.dock?.clientModuleResolution;
79
- const entries = [];
80
- for (const [id, view] of ctx.docks.views) {
81
- const scripts = [];
82
- const anyView = view;
83
- if (anyView.clientScript) scripts.push(anyView.clientScript);
84
- if (anyView.action) scripts.push(anyView.action);
85
- if (anyView.renderer) scripts.push(anyView.renderer);
86
- if (scripts.length === 0) continue;
87
- const thunks = scripts.map((script) => `() => import(${JSON.stringify(resolveClientModuleSpecifier(script.importFrom, { template }))})`);
88
- entries.push(` ${JSON.stringify(id)}: [${thunks.join(", ")}],`);
89
- }
90
- return `// Generated by @devframes/hub — dock client-script import map.\nexport const clientImports = {\n${entries.join("\n")}\n}\nexport default clientImports\n`;
91
- }
92
- /**
93
- * Initiate a hub instance — the whole multi-devframe devtools surface
59
+ * Initiate a hub instance: the whole multi-devframe devtools surface
94
60
  * behind one framework-agnostic, web-standard handler. Every mounted
95
61
  * devframe shares one context (merged RPC registry, shared state, docks /
96
62
  * terminals / messages / commands), one WebSocket transport, and one Auth;
97
63
  * the instance serves each frame's SPA at `<base><id>/`, the discovery
98
64
  * endpoints (`__connection.json`, `__index.json`, `__client-imports.js`),
99
65
  * the aggregate MCP route, and whatever the {@link DevframeHubUi} slot
100
- * provides — the hub itself stays headless.
66
+ * provides, while the hub itself stays headless.
101
67
  *
102
68
  * The factory is synchronous and initializes eagerly, and binds no port of
103
69
  * its own: the WebSocket follows `server` / `ws` (see
@@ -127,9 +93,14 @@ function initHub(options) {
127
93
  ...options.name !== void 0 ? { name: options.name } : {},
128
94
  rootDir: cwd
129
95
  }),
96
+ /**
97
+ * One meta document is served from the hub base *and* from every frame
98
+ * base, so the advertised WS path has to be base-absolute to resolve to
99
+ * the same socket from any depth.
100
+ */
130
101
  absoluteWsPath: true,
131
102
  resolveSidecarPort: async (sidecarHost) => {
132
- const { getPort } = await import("../dist-R0T2g9Vw.mjs");
103
+ const { getPort } = await import("../dist-BP3CGpgZ.mjs");
133
104
  return getPort({
134
105
  port: 9777,
135
106
  portRange: [9777, 9877],
@@ -140,54 +111,14 @@ function initHub(options) {
140
111
  throw diagnostics.DF8003();
141
112
  },
142
113
  async init(api) {
143
- if (options.context && options.devframes?.length) throw diagnostics.DF8002();
144
- let ctx;
145
- if (options.context) ctx = options.context;
146
- else {
147
- const host = {
148
- ...createH3DevframeHost({
149
- origin: () => api.origin() ?? "http://localhost",
150
- appName: "devframes",
151
- workspaceRoot: cwd,
152
- mount: (mountBase, dir) => {
153
- mountStaticHandler(app, mountBase, dir);
154
- }
155
- }),
156
- ...options.getStorageDir ? { getStorageDir: options.getStorageDir } : {},
157
- mountConnectionMeta: (frameBase) => {
158
- app.use(joinURL(frameBase, DEVFRAME_CONNECTION_META_FILENAME), () => api.connectionMeta());
159
- }
160
- };
161
- ctx = await createHubContext({
162
- cwd,
163
- workspaceRoot: cwd,
164
- mode: "dev",
165
- host,
166
- ...options.rpcDeclarations ? { builtinRpcDeclarations: options.rpcDeclarations } : {}
167
- });
168
- }
114
+ const ctx = await resolveHubContext(options, app, cwd, api);
169
115
  if (options.clientModuleResolution) ctx.staticConfig.dock = {
170
116
  ...ctx.staticConfig.dock,
171
117
  clientModuleResolution: options.clientModuleResolution
172
118
  };
173
119
  const devframes = await resolveDevframesInput(options.devframes ?? []);
174
120
  for (const input of options.services ?? []) ctx.services.install(input);
175
- const setups = [];
176
- for (const { devframe: def, dock } of devframes) {
177
- if (RESERVED_HUB_PATHS.includes(def.id)) throw diagnostics.DF8000({ id: def.id });
178
- if (!/^[\w.-]+$/.test(def.id)) throw diagnostics.DF8004({ id: def.id });
179
- const frameBase = withTrailingSlash(joinURL(base, def.id));
180
- const run = await prepareDevframe(ctx, def, {
181
- base: frameBase,
182
- ...dock ? { dock } : {}
183
- });
184
- if (run) setups.push(run);
185
- frames.push({
186
- id: def.id,
187
- base: frameBase,
188
- title: def.name
189
- });
190
- }
121
+ const setups = await mountDevframes(ctx, devframes, base, frames, options.mcp !== false);
191
122
  await ctx.services.ready();
192
123
  for (const run of setups) await run();
193
124
  await options.configure?.(ctx);
@@ -198,14 +129,21 @@ function initHub(options) {
198
129
  ...registration.importName ? { importName: registration.importName } : {}
199
130
  };
200
131
  (await ctx.rpc.sharedState.get(DOCK_RENDERERS_STATE_KEY, { initialValue: {} })).mutate(() => manifest);
201
- const mcpConfig = options.mcp === true ? {} : options.mcp;
132
+ const mcpSetting = options.mcp ?? "auto";
133
+ let mcpModule;
134
+ let mcpConfig;
135
+ if (mcpSetting === "auto") {
136
+ mcpModule = await loadAutoMcpAdapter(ctx.agent);
137
+ if (mcpModule) mcpConfig = { authorization: false };
138
+ } else mcpConfig = resolveMcpConfig(mcpSetting);
202
139
  if (!mcpConfig) return { context: ctx };
203
140
  const mcpRoute = withoutLeadingSlash(mcpConfig.path ?? DEVFRAME_MCP_ROUTE);
204
- const { mountMcpHttp } = await importRuntimeModule("devframe/adapters/mcp");
205
- const mounted = mountMcpHttp(app, ctx, joinURL(base, mcpRoute), {
141
+ mcpModule ??= await importRuntimeModule("devframe/adapters/mcp");
142
+ const mounted = mcpModule.mountMcpHttp(app, ctx, joinURL(base, mcpRoute), {
206
143
  serverName: options.name ?? "devframes-hub",
207
144
  serverVersion: options.version ?? "0.0.0",
208
145
  exposeSharedState: true,
146
+ authorization: mcpConfig.authorization,
209
147
  allowedOrigins: mcpConfig.allowedOrigins
210
148
  });
211
149
  return {
@@ -2,7 +2,7 @@ import { REMOTE_CONNECTION_KEY } from "devframe/constants";
2
2
  //#region src/client-modules.ts
3
3
  const URL_SCHEME_RE = /^[a-z][a-z\d+.-]*:/i;
4
4
  /**
5
- * Whether a client-script `importFrom` is a **bare module specifier** — an
5
+ * Whether a client-script `importFrom` is a **bare module specifier**: an
6
6
  * npm-style name (`vite-plugin-vue-tracer/client/vite-devtools`) rather than
7
7
  * a URL the browser can resolve natively.
8
8
  */
@@ -15,10 +15,10 @@ function isBareModuleSpecifier(specifier) {
15
15
  * specifiers pass through untouched; a **bare** specifier resolves through
16
16
  * the explicit `resolveClientModule` callback (a viewer's own policy), then
17
17
  * the host-advertised `template` (`ConnectionMeta.configs.dock.clientModuleResolution`,
18
- * its `{specifier}` token replaced — e.g. Vite's `'/@id/{specifier}'`),
18
+ * its `{specifier}` token replaced, e.g. Vite's `'/@id/{specifier}'`),
19
19
  * resolved against `metaBaseUrl` so it targets the host's origin from any
20
- * viewer. With neither, it stays unchanged — a page import map may still
21
- * cover it.
20
+ * viewer. With neither, it stays unchanged, since a page import map may
21
+ * still cover it.
22
22
  */
23
23
  function resolveClientModuleSpecifier(specifier, options = {}) {
24
24
  if (!isBareModuleSpecifier(specifier)) return specifier;
@@ -40,8 +40,8 @@ function resolveClientModuleSpecifier(specifier, options = {}) {
40
40
  */
41
41
  function clientScriptFailureHint(importFrom, specifier) {
42
42
  if (!isBareModuleSpecifier(importFrom)) return "";
43
- if (specifier === importFrom) return " — the specifier is a bare npm specifier and this host advertises no client-module resolution (`ConnectionMeta.configs.dock.clientModuleResolution`). Serve the script as a self-contained bundle by URL, pass `resolveClientModule` to this client host, or run under a host that resolves bare specifiers (e.g. Vite: `/@id/{specifier}`).";
44
- return ` — the host resolved the bare specifier "${importFrom}" to this URL but could not serve the module. Check the package is installed and resolvable from the host project's root (and built, if its exports point at build output).`;
43
+ if (specifier === importFrom) return ". The specifier is a bare npm specifier and this host advertises no client-module resolution (`ConnectionMeta.configs.dock.clientModuleResolution`). Serve the script as a self-contained bundle by URL, pass `resolveClientModule` to this client host, or run under a host that resolves bare specifiers (e.g. Vite: `/@id/{specifier}`).";
44
+ return `. The host resolved the bare specifier "${importFrom}" to this URL but could not serve the module. Check the package is installed and resolvable from the host project's root (and built, if its exports point at build output).`;
45
45
  }
46
46
  //#endregion
47
47
  //#region src/remote-url.ts
@@ -77,42 +77,37 @@ function buildRemoteConnectionUrl(baseUrl, payload, transport = "fragment") {
77
77
  const beforeHash = hashIdx === -1 ? baseUrl : baseUrl.slice(0, hashIdx);
78
78
  return `${beforeHash}${beforeHash.includes("?") ? "&" : "?"}${param}${hash}`;
79
79
  }
80
+ function stripParamList(value) {
81
+ const parts = value.split("&");
82
+ const filtered = parts.filter((part) => part.split("=")[0] !== REMOTE_CONNECTION_KEY);
83
+ return [filtered.join("&"), filtered.length !== parts.length];
84
+ }
85
+ /** Strip the descriptor from a URL fragment, preserving its route/query split. */
86
+ function stripHashParams(hash) {
87
+ const routeQueryIdx = hash.indexOf("?");
88
+ if (routeQueryIdx === -1) return stripParamList(hash);
89
+ const route = hash.slice(0, routeQueryIdx);
90
+ const [query, changed] = stripParamList(hash.slice(routeQueryIdx + 1));
91
+ if (!changed) return [hash, false];
92
+ return [query ? `${route}?${query}` : route, true];
93
+ }
94
+ /** Strip the descriptor from a URL query string (the part before any fragment). */
95
+ function stripQueryParams(beforeHash) {
96
+ const queryIdx = beforeHash.indexOf("?");
97
+ if (queryIdx === -1) return [beforeHash, false];
98
+ const path = beforeHash.slice(0, queryIdx);
99
+ const [query, changed] = stripParamList(beforeHash.slice(queryIdx + 1));
100
+ if (!changed) return [beforeHash, false];
101
+ return [query ? `${path}?${query}` : path, true];
102
+ }
80
103
  /** Remove the remote connection descriptor from a URL before displaying or copying it. */
81
104
  function stripRemoteConnectionFromUrl(input) {
82
- function stripParamList(value) {
83
- const parts = value.split("&");
84
- const filtered = parts.filter((part) => part.split("=")[0] !== REMOTE_CONNECTION_KEY);
85
- return [filtered.join("&"), filtered.length !== parts.length];
86
- }
87
105
  const hashIdx = input.indexOf("#");
88
- let beforeHash = hashIdx === -1 ? input : input.slice(0, hashIdx);
89
- let hash = hashIdx === -1 ? void 0 : input.slice(hashIdx + 1);
90
- let changed = false;
91
- if (hash !== void 0) {
92
- const routeQueryIdx = hash.indexOf("?");
93
- if (routeQueryIdx === -1) {
94
- const [nextHash, didChange] = stripParamList(hash);
95
- hash = nextHash;
96
- changed ||= didChange;
97
- } else {
98
- const route = hash.slice(0, routeQueryIdx);
99
- const [query, didChange] = stripParamList(hash.slice(routeQueryIdx + 1));
100
- if (didChange) {
101
- hash = query ? `${route}?${query}` : route;
102
- changed = true;
103
- }
104
- }
105
- }
106
- const queryIdx = beforeHash.indexOf("?");
107
- if (queryIdx !== -1) {
108
- const path = beforeHash.slice(0, queryIdx);
109
- const [query, didChange] = stripParamList(beforeHash.slice(queryIdx + 1));
110
- if (didChange) {
111
- beforeHash = query ? `${path}?${query}` : path;
112
- changed = true;
113
- }
114
- }
115
- if (!changed) return input;
106
+ const rawHash = hashIdx === -1 ? void 0 : input.slice(hashIdx + 1);
107
+ const rawBeforeHash = hashIdx === -1 ? input : input.slice(0, hashIdx);
108
+ const [hash, hashChanged] = rawHash === void 0 ? [void 0, false] : stripHashParams(rawHash);
109
+ const [beforeHash, queryChanged] = stripQueryParams(rawBeforeHash);
110
+ if (!hashChanged && !queryChanged) return input;
116
111
  return hash ? `${beforeHash}#${hash}` : beforeHash;
117
112
  }
118
113
  //#endregion