@ethisyscore/extension-runtime 1.6.2 → 1.7.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.
@@ -1,6 +1,7 @@
1
1
  import { M as McpTransport } from '../transport-DVn2GVZh.cjs';
2
2
  import { ReactNode } from 'react';
3
3
  import { SduiNode, RenderMode } from '@ethisyscore/protocol';
4
+ import { RemoteConnection } from '@remote-dom/core';
4
5
 
5
6
  interface ExtensionRuntimeProviderProps {
6
7
  transport: McpTransport;
@@ -75,6 +76,66 @@ interface UseMcpToolResult<TReq, TRes> {
75
76
  */
76
77
  declare function useMcpTool<TReq, TRes>(toolName: string, opts?: UseMcpToolOptions): UseMcpToolResult<TReq, TRes>;
77
78
 
79
+ /**
80
+ * Result shape returned by {@link useMcpQuery}. Mirrors the host-app
81
+ * `react-query`-style envelope so consumers can render loading / error /
82
+ * data states declaratively. `refetch` is stable across renders.
83
+ */
84
+ interface UseMcpQueryResult<TRes> {
85
+ data: TRes | undefined;
86
+ loading: boolean;
87
+ error: Error | undefined;
88
+ /** Re-run the fetch against the current args. Stable across renders. */
89
+ refetch: () => void;
90
+ }
91
+ /**
92
+ * Options for {@link useMcpQuery}.
93
+ */
94
+ interface UseMcpQueryOptions {
95
+ /**
96
+ * Gate the fetch on a guard. When false, the hook returns idle state and
97
+ * never invokes — important because hooks must be called unconditionally
98
+ * at the top of the component, so an `enabled` flag is the standard way
99
+ * to express "fetch only when a selection is present" without violating
100
+ * the rules of hooks. Defaults to `true`.
101
+ */
102
+ enabled?: boolean;
103
+ }
104
+ /**
105
+ * Auto-fetch wrapper over the imperative {@link useMcpTool}. Fires the tool
106
+ * call on mount AND whenever the serialised `args` change. Use for
107
+ * read-shaped MCP tool calls that back a component's render data; for
108
+ * mutations, call {@link useMcpTool} directly and trigger
109
+ * `invoke(...)` from event handlers.
110
+ *
111
+ * Why this lives alongside {@link useMcpTool}: many plugin MCP tools are
112
+ * effectively queries (`list-tasks`, `get-pending-approvals`, …) — paginated
113
+ * lookups that change with filter inputs. The base hook's imperative
114
+ * `invoke` signature is correct for mutations but ergonomically wrong for
115
+ * reads, where every consumer ends up writing the same `useEffect` +
116
+ * AbortController + cancellation-on-unmount boilerplate. This hook absorbs
117
+ * that boilerplate once.
118
+ *
119
+ * @template TArgs The request shape sent to the tool.
120
+ * @template TRes The response shape the tool returns.
121
+ */
122
+ declare function useMcpQuery<TArgs, TRes>(toolName: string, args: TArgs, options?: UseMcpQueryOptions): UseMcpQueryResult<TRes>;
123
+ /**
124
+ * Envelope shape that covers both `{ items: T[] }` and `{ rows: T[] }`
125
+ * conventions emitted by platform / plugin MCP read tools. Optional both
126
+ * sides so empty responses still type-check.
127
+ */
128
+ interface ItemsResponse<T> {
129
+ items?: T[];
130
+ rows?: T[];
131
+ }
132
+ /**
133
+ * Normalise an {@link ItemsResponse} envelope to a plain array.
134
+ * Returns `[]` when the response is `undefined` (typical pre-first-fetch
135
+ * state) or when neither field is populated.
136
+ */
137
+ declare function unwrapItems<T>(response: ItemsResponse<T> | undefined): T[];
138
+
78
139
  /**
79
140
  * Configuration accepted by {@link defineDeclarativePlugin}.
80
141
  *
@@ -117,4 +178,223 @@ declare const defineDeclarativePlugin: (cfg: DeclarativePluginConfig) => Declara
117
178
  */
118
179
  declare const defineEthisysPlugin: <TMount>(cfg: EthisysPluginConfig<TMount>) => EthisysPluginConfig<TMount>;
119
180
 
120
- export { type DeclarativePluginConfig, type EthisysPluginConfig, ExtensionRuntimeProvider, type ExtensionRuntimeProviderProps, McpTransport, type UseMcpResourceOptions, type UseMcpResourceResult, type UseMcpToolOptions, type UseMcpToolResult, defineDeclarativePlugin, defineEthisysPlugin, useMcpResource, useMcpTool };
181
+ /**
182
+ * Contract B (worker remote-runtime) plugin-side React root.
183
+ *
184
+ * # The problem this solves
185
+ *
186
+ * A Contract B plugin runs in a sandboxed Web Worker — no `document`, no
187
+ * `window`, no DOM. The host owns rendering: the worker constructs a
188
+ * `RemoteElement` tree via `@remote-dom/core`, mutations forward over a
189
+ * `MessagePort`, the host receives them and commits them to a real React tree
190
+ * (see `coreconnect-web/src/extensions/runtime/WorkerSurfaceMount.tsx` for the
191
+ * receiver side).
192
+ *
193
+ * Until now plugin authors had to hand-author the `RemoteElement` construction
194
+ * + mutation forwarding manually. `@remote-dom/react` ships only the host-side
195
+ * primitives (`createRemoteComponent`, `RemoteRootRenderer`, etc.) — there is
196
+ * no worker-side React reconciler in the package. This helper provides one.
197
+ *
198
+ * # What this is
199
+ *
200
+ * The public surface (`createRemoteRoot(port, options)`) plus a working
201
+ * `react-reconciler` HostConfig that commits to a Remote DOM tree and
202
+ * forwards mutation records over the `MessagePort` to the host receiver.
203
+ * Authors call `root.render(<App />)` and the host's `WorkerSurfaceMount`
204
+ * sees the result.
205
+ *
206
+ * # What's NOT plumbed yet (Phase 1 limitation)
207
+ *
208
+ * Event-listener round-trip. The host transport currently has no
209
+ * `ethisys:remotedom:call` channel — `WorkerRemoteDomTransport` only
210
+ * forwards `ethisys:remotedom` payloads from worker → host, never the
211
+ * other direction. So a worker-side `onClick={() => ...}` is **registered
212
+ * locally** but the host can't call it. The reconciler retains every
213
+ * listener on its in-memory `RemoteElementInstance` so the wiring is ready
214
+ * the moment the call channel lands; until then, plugin authors should
215
+ * keep interactive surfaces on Contract A.
216
+ *
217
+ * # The API
218
+ *
219
+ * ```ts
220
+ * import { createRemoteRoot } from "@ethisyscore/extension-runtime/plugin";
221
+ *
222
+ * export async function activate(port: MessagePort): Promise<void> {
223
+ * const root = createRemoteRoot(port);
224
+ * root.render(<App />);
225
+ * }
226
+ * ```
227
+ *
228
+ * `<App />` is plain React. Any component shipped by the host's frozen
229
+ * import-map allowlist (the closed Contract B primitive vocabulary —
230
+ * `Button`, `DataTable`, `Form`, `Card`, `Tabs`, `Select`, `Alert`, etc.)
231
+ * renders. The reconciler walks the React tree and commits to a
232
+ * `RemoteRootElement`; mutation records forward over the port; the host's
233
+ * `RemoteReceiver` commits the result into the real React tree.
234
+ *
235
+ * # Wire shape
236
+ *
237
+ * Every reconciler mutation is posted as
238
+ * `{ type: "ethisys:remotedom", payload: RemoteMutationRecord[] }` to match
239
+ * `WorkerRemoteDomTransport`'s `RemoteDomMessage` contract — the host
240
+ * extracts `payload` and feeds it straight into `receiver.mutate(records)`.
241
+ * `BatchingRemoteConnection` (controlled via `options.batchMutations`)
242
+ * collapses contiguous mutations into a single port post per commit.
243
+ */
244
+
245
+ /**
246
+ * Options for {@link createRemoteRoot}. Reserved for forward compatibility —
247
+ * Phase 1 ships with zero required options. Adding fields here is additive;
248
+ * removing them is a breaking change.
249
+ */
250
+ interface CreateRemoteRootOptions {
251
+ /**
252
+ * When supplied, the reconciler batches contiguous mutations into a single
253
+ * `port.postMessage` rather than firing one per mutation. Default `true`
254
+ * — measurably reduces host-side commit cost on the first render of a
255
+ * non-trivial tree.
256
+ */
257
+ batchMutations?: boolean;
258
+ /**
259
+ * Override the connection factory. Reserved for tests; production callers
260
+ * never pass this. The factory's contract is "build a RemoteConnection
261
+ * that forwards mutations over the supplied port"; the default uses
262
+ * `createRemoteConnection` from `@remote-dom/core`.
263
+ */
264
+ connectionFactory?: (port: MessagePort) => RemoteConnection;
265
+ }
266
+ /**
267
+ * Plugin-side React root. Mirrors the shape of `ReactDOMClient.Root` so
268
+ * authors familiar with `createRoot().render(...)` find the same API on the
269
+ * worker side.
270
+ */
271
+ interface RemoteRoot {
272
+ /**
273
+ * Render a React element tree against the worker's `RemoteRootElement`.
274
+ * The reconciler commits to the root, the mutation observer forwards
275
+ * mutations over the port, the host re-renders. Idempotent — calling
276
+ * `render` twice with the same element is fine; the reconciler dedupes.
277
+ */
278
+ render(element: ReactNode): void;
279
+ /**
280
+ * Tear down the reconciled tree and stop forwarding mutations. Authors
281
+ * should call this from a `Symbol.dispose` or equivalent when the worker
282
+ * is shutting down — leaking the reconciler holds the `MessagePort` open
283
+ * and prevents the worker from being collected.
284
+ */
285
+ unmount(): void;
286
+ }
287
+ /**
288
+ * Construct a plugin-side React root that commits to a `RemoteRootElement`
289
+ * and forwards mutations over the supplied `MessagePort`.
290
+ *
291
+ * **API stability:** the function signature is stable for Phase 1. The
292
+ * options bag is forward-compatible (additive only).
293
+ *
294
+ * **Implementation status:** the connection + root construction lands in this
295
+ * commit. The `react-reconciler` `HostConfig` is a scaffold — `render()`
296
+ * throws a structured error directing authors to the W1A tracking issue.
297
+ * Authors should treat this commit as "the API is locked, the reconciler is
298
+ * being authored." See follow-on commits on the `feature/contract-b-create-remote-root-w1a`
299
+ * branch.
300
+ */
301
+ declare function createRemoteRoot(port: MessagePort, options?: CreateRemoteRootOptions): RemoteRoot;
302
+
303
+ /**
304
+ * W1B — MessagePort-backed McpTransport for Contract B (worker remote-runtime)
305
+ * plugins.
306
+ *
307
+ * The plugin-side React hooks (`useMcpResource`, `useMcpTool`) consume an
308
+ * {@link McpTransport} — an abstraction over the host call. Contract A
309
+ * (host-rendered) plugins pick up the host-injected transport via
310
+ * `ExtensionRuntimeProvider`. Contract B plugins run in a Web Worker with
311
+ * no shared object surface — the only channel is the `MessagePort` the
312
+ * host transferred via `activate(port)`. This helper bridges the two
313
+ * worlds: it exposes the {@link McpTransport} contract on the worker side
314
+ * and serialises every call into a request/response envelope over the
315
+ * port.
316
+ *
317
+ * # The protocol on the wire
318
+ *
319
+ * The wire shape mirrors `WorkerRemoteDomTransport` (in `host/worker/transport.ts`)
320
+ * one-for-one — that transport is the host receiver and decides what a "well-
321
+ * formed" message looks like. Two request shapes, two `:result`-suffixed reply
322
+ * shapes, one abort envelope. Each call mints a fresh request id (`req-{n}`).
323
+ *
324
+ * ```jsonc
325
+ * // worker → host
326
+ * { type: "ethisys:mcp:getResource", id: "req-3", uri: "tickets://home" }
327
+ * { type: "ethisys:mcp:invokeTool", id: "req-4", name: "tickets:open", args: { ... } }
328
+ *
329
+ * // host → worker (matching id, suffixed type)
330
+ * { type: "ethisys:mcp:getResource:result", id: "req-3", ok: true, data: { uri, data } }
331
+ * { type: "ethisys:mcp:invokeTool:result", id: "req-4", ok: false, error: "..." }
332
+ * ```
333
+ *
334
+ * Requests honour the optional `AbortSignal`. On abort, the transport posts
335
+ * a `{ type: "ethisys:mcp:abort", id }` envelope so the host can cancel
336
+ * in-flight work, then rejects the pending promise with an `AbortError`-shaped
337
+ * `Error` so the consuming hooks see the same shape as native fetch cancellation.
338
+ *
339
+ * # Authoring shape
340
+ *
341
+ * ```ts
342
+ * export async function activate(port: MessagePort): Promise<void> {
343
+ * const transport = createPortMcpTransport(port);
344
+ * const root = createRemoteRoot(port);
345
+ * root.render(
346
+ * <ExtensionRuntimeProvider transport={transport}>
347
+ * <App />
348
+ * </ExtensionRuntimeProvider>,
349
+ * );
350
+ * }
351
+ * ```
352
+ *
353
+ * `<App />` uses `useMcpResource` / `useMcpTool` as it would on the host
354
+ * side; the transport translates each call into the port envelope.
355
+ */
356
+
357
+ /**
358
+ * Options for {@link createPortMcpTransport}. Reserved for forward
359
+ * compatibility — the public surface is empty in Phase 1.
360
+ */
361
+ interface CreatePortMcpTransportOptions {
362
+ /**
363
+ * Override the request-id generator. Reserved for tests so they can
364
+ * make request ids deterministic. Production callers never pass this.
365
+ */
366
+ requestIdFactory?: () => string;
367
+ /**
368
+ * Override the port's `addEventListener` / `removeEventListener` /
369
+ * `postMessage` triple. Reserved for tests so the transport can be
370
+ * exercised without instantiating a real MessageChannel.
371
+ */
372
+ portShimForTests?: PortShim;
373
+ }
374
+ /**
375
+ * Minimal subset of the MessagePort surface the transport actually uses.
376
+ * Exposed so tests can construct a polyfill without faking the full
377
+ * MessagePort.
378
+ */
379
+ interface PortShim {
380
+ addEventListener(type: "message", listener: (event: {
381
+ data: unknown;
382
+ }) => void): void;
383
+ removeEventListener(type: "message", listener: (event: {
384
+ data: unknown;
385
+ }) => void): void;
386
+ postMessage(value: unknown): void;
387
+ }
388
+ /**
389
+ * Construct an {@link McpTransport} backed by a MessagePort.
390
+ *
391
+ * @param port The host-transferred MessagePort for the worker surface.
392
+ * @param options Reserved for forward compatibility / test injection.
393
+ *
394
+ * @returns A transport implementation honouring the {@link McpTransport}
395
+ * contract — fetch a resource, invoke a tool, observe abort
396
+ * signals, settle the promise on the host's reply envelope.
397
+ */
398
+ declare function createPortMcpTransport(port: MessagePort | PortShim, options?: CreatePortMcpTransportOptions): McpTransport;
399
+
400
+ export { type CreatePortMcpTransportOptions, type CreateRemoteRootOptions, type DeclarativePluginConfig, type EthisysPluginConfig, ExtensionRuntimeProvider, type ExtensionRuntimeProviderProps, type ItemsResponse, McpTransport, type PortShim, type RemoteRoot, type UseMcpQueryOptions, type UseMcpQueryResult, type UseMcpResourceOptions, type UseMcpResourceResult, type UseMcpToolOptions, type UseMcpToolResult, createPortMcpTransport, createRemoteRoot, defineDeclarativePlugin, defineEthisysPlugin, unwrapItems, useMcpQuery, useMcpResource, useMcpTool };
@@ -1,6 +1,7 @@
1
1
  import { M as McpTransport } from '../transport-DVn2GVZh.js';
2
2
  import { ReactNode } from 'react';
3
3
  import { SduiNode, RenderMode } from '@ethisyscore/protocol';
4
+ import { RemoteConnection } from '@remote-dom/core';
4
5
 
5
6
  interface ExtensionRuntimeProviderProps {
6
7
  transport: McpTransport;
@@ -75,6 +76,66 @@ interface UseMcpToolResult<TReq, TRes> {
75
76
  */
76
77
  declare function useMcpTool<TReq, TRes>(toolName: string, opts?: UseMcpToolOptions): UseMcpToolResult<TReq, TRes>;
77
78
 
79
+ /**
80
+ * Result shape returned by {@link useMcpQuery}. Mirrors the host-app
81
+ * `react-query`-style envelope so consumers can render loading / error /
82
+ * data states declaratively. `refetch` is stable across renders.
83
+ */
84
+ interface UseMcpQueryResult<TRes> {
85
+ data: TRes | undefined;
86
+ loading: boolean;
87
+ error: Error | undefined;
88
+ /** Re-run the fetch against the current args. Stable across renders. */
89
+ refetch: () => void;
90
+ }
91
+ /**
92
+ * Options for {@link useMcpQuery}.
93
+ */
94
+ interface UseMcpQueryOptions {
95
+ /**
96
+ * Gate the fetch on a guard. When false, the hook returns idle state and
97
+ * never invokes — important because hooks must be called unconditionally
98
+ * at the top of the component, so an `enabled` flag is the standard way
99
+ * to express "fetch only when a selection is present" without violating
100
+ * the rules of hooks. Defaults to `true`.
101
+ */
102
+ enabled?: boolean;
103
+ }
104
+ /**
105
+ * Auto-fetch wrapper over the imperative {@link useMcpTool}. Fires the tool
106
+ * call on mount AND whenever the serialised `args` change. Use for
107
+ * read-shaped MCP tool calls that back a component's render data; for
108
+ * mutations, call {@link useMcpTool} directly and trigger
109
+ * `invoke(...)` from event handlers.
110
+ *
111
+ * Why this lives alongside {@link useMcpTool}: many plugin MCP tools are
112
+ * effectively queries (`list-tasks`, `get-pending-approvals`, …) — paginated
113
+ * lookups that change with filter inputs. The base hook's imperative
114
+ * `invoke` signature is correct for mutations but ergonomically wrong for
115
+ * reads, where every consumer ends up writing the same `useEffect` +
116
+ * AbortController + cancellation-on-unmount boilerplate. This hook absorbs
117
+ * that boilerplate once.
118
+ *
119
+ * @template TArgs The request shape sent to the tool.
120
+ * @template TRes The response shape the tool returns.
121
+ */
122
+ declare function useMcpQuery<TArgs, TRes>(toolName: string, args: TArgs, options?: UseMcpQueryOptions): UseMcpQueryResult<TRes>;
123
+ /**
124
+ * Envelope shape that covers both `{ items: T[] }` and `{ rows: T[] }`
125
+ * conventions emitted by platform / plugin MCP read tools. Optional both
126
+ * sides so empty responses still type-check.
127
+ */
128
+ interface ItemsResponse<T> {
129
+ items?: T[];
130
+ rows?: T[];
131
+ }
132
+ /**
133
+ * Normalise an {@link ItemsResponse} envelope to a plain array.
134
+ * Returns `[]` when the response is `undefined` (typical pre-first-fetch
135
+ * state) or when neither field is populated.
136
+ */
137
+ declare function unwrapItems<T>(response: ItemsResponse<T> | undefined): T[];
138
+
78
139
  /**
79
140
  * Configuration accepted by {@link defineDeclarativePlugin}.
80
141
  *
@@ -117,4 +178,223 @@ declare const defineDeclarativePlugin: (cfg: DeclarativePluginConfig) => Declara
117
178
  */
118
179
  declare const defineEthisysPlugin: <TMount>(cfg: EthisysPluginConfig<TMount>) => EthisysPluginConfig<TMount>;
119
180
 
120
- export { type DeclarativePluginConfig, type EthisysPluginConfig, ExtensionRuntimeProvider, type ExtensionRuntimeProviderProps, McpTransport, type UseMcpResourceOptions, type UseMcpResourceResult, type UseMcpToolOptions, type UseMcpToolResult, defineDeclarativePlugin, defineEthisysPlugin, useMcpResource, useMcpTool };
181
+ /**
182
+ * Contract B (worker remote-runtime) plugin-side React root.
183
+ *
184
+ * # The problem this solves
185
+ *
186
+ * A Contract B plugin runs in a sandboxed Web Worker — no `document`, no
187
+ * `window`, no DOM. The host owns rendering: the worker constructs a
188
+ * `RemoteElement` tree via `@remote-dom/core`, mutations forward over a
189
+ * `MessagePort`, the host receives them and commits them to a real React tree
190
+ * (see `coreconnect-web/src/extensions/runtime/WorkerSurfaceMount.tsx` for the
191
+ * receiver side).
192
+ *
193
+ * Until now plugin authors had to hand-author the `RemoteElement` construction
194
+ * + mutation forwarding manually. `@remote-dom/react` ships only the host-side
195
+ * primitives (`createRemoteComponent`, `RemoteRootRenderer`, etc.) — there is
196
+ * no worker-side React reconciler in the package. This helper provides one.
197
+ *
198
+ * # What this is
199
+ *
200
+ * The public surface (`createRemoteRoot(port, options)`) plus a working
201
+ * `react-reconciler` HostConfig that commits to a Remote DOM tree and
202
+ * forwards mutation records over the `MessagePort` to the host receiver.
203
+ * Authors call `root.render(<App />)` and the host's `WorkerSurfaceMount`
204
+ * sees the result.
205
+ *
206
+ * # What's NOT plumbed yet (Phase 1 limitation)
207
+ *
208
+ * Event-listener round-trip. The host transport currently has no
209
+ * `ethisys:remotedom:call` channel — `WorkerRemoteDomTransport` only
210
+ * forwards `ethisys:remotedom` payloads from worker → host, never the
211
+ * other direction. So a worker-side `onClick={() => ...}` is **registered
212
+ * locally** but the host can't call it. The reconciler retains every
213
+ * listener on its in-memory `RemoteElementInstance` so the wiring is ready
214
+ * the moment the call channel lands; until then, plugin authors should
215
+ * keep interactive surfaces on Contract A.
216
+ *
217
+ * # The API
218
+ *
219
+ * ```ts
220
+ * import { createRemoteRoot } from "@ethisyscore/extension-runtime/plugin";
221
+ *
222
+ * export async function activate(port: MessagePort): Promise<void> {
223
+ * const root = createRemoteRoot(port);
224
+ * root.render(<App />);
225
+ * }
226
+ * ```
227
+ *
228
+ * `<App />` is plain React. Any component shipped by the host's frozen
229
+ * import-map allowlist (the closed Contract B primitive vocabulary —
230
+ * `Button`, `DataTable`, `Form`, `Card`, `Tabs`, `Select`, `Alert`, etc.)
231
+ * renders. The reconciler walks the React tree and commits to a
232
+ * `RemoteRootElement`; mutation records forward over the port; the host's
233
+ * `RemoteReceiver` commits the result into the real React tree.
234
+ *
235
+ * # Wire shape
236
+ *
237
+ * Every reconciler mutation is posted as
238
+ * `{ type: "ethisys:remotedom", payload: RemoteMutationRecord[] }` to match
239
+ * `WorkerRemoteDomTransport`'s `RemoteDomMessage` contract — the host
240
+ * extracts `payload` and feeds it straight into `receiver.mutate(records)`.
241
+ * `BatchingRemoteConnection` (controlled via `options.batchMutations`)
242
+ * collapses contiguous mutations into a single port post per commit.
243
+ */
244
+
245
+ /**
246
+ * Options for {@link createRemoteRoot}. Reserved for forward compatibility —
247
+ * Phase 1 ships with zero required options. Adding fields here is additive;
248
+ * removing them is a breaking change.
249
+ */
250
+ interface CreateRemoteRootOptions {
251
+ /**
252
+ * When supplied, the reconciler batches contiguous mutations into a single
253
+ * `port.postMessage` rather than firing one per mutation. Default `true`
254
+ * — measurably reduces host-side commit cost on the first render of a
255
+ * non-trivial tree.
256
+ */
257
+ batchMutations?: boolean;
258
+ /**
259
+ * Override the connection factory. Reserved for tests; production callers
260
+ * never pass this. The factory's contract is "build a RemoteConnection
261
+ * that forwards mutations over the supplied port"; the default uses
262
+ * `createRemoteConnection` from `@remote-dom/core`.
263
+ */
264
+ connectionFactory?: (port: MessagePort) => RemoteConnection;
265
+ }
266
+ /**
267
+ * Plugin-side React root. Mirrors the shape of `ReactDOMClient.Root` so
268
+ * authors familiar with `createRoot().render(...)` find the same API on the
269
+ * worker side.
270
+ */
271
+ interface RemoteRoot {
272
+ /**
273
+ * Render a React element tree against the worker's `RemoteRootElement`.
274
+ * The reconciler commits to the root, the mutation observer forwards
275
+ * mutations over the port, the host re-renders. Idempotent — calling
276
+ * `render` twice with the same element is fine; the reconciler dedupes.
277
+ */
278
+ render(element: ReactNode): void;
279
+ /**
280
+ * Tear down the reconciled tree and stop forwarding mutations. Authors
281
+ * should call this from a `Symbol.dispose` or equivalent when the worker
282
+ * is shutting down — leaking the reconciler holds the `MessagePort` open
283
+ * and prevents the worker from being collected.
284
+ */
285
+ unmount(): void;
286
+ }
287
+ /**
288
+ * Construct a plugin-side React root that commits to a `RemoteRootElement`
289
+ * and forwards mutations over the supplied `MessagePort`.
290
+ *
291
+ * **API stability:** the function signature is stable for Phase 1. The
292
+ * options bag is forward-compatible (additive only).
293
+ *
294
+ * **Implementation status:** the connection + root construction lands in this
295
+ * commit. The `react-reconciler` `HostConfig` is a scaffold — `render()`
296
+ * throws a structured error directing authors to the W1A tracking issue.
297
+ * Authors should treat this commit as "the API is locked, the reconciler is
298
+ * being authored." See follow-on commits on the `feature/contract-b-create-remote-root-w1a`
299
+ * branch.
300
+ */
301
+ declare function createRemoteRoot(port: MessagePort, options?: CreateRemoteRootOptions): RemoteRoot;
302
+
303
+ /**
304
+ * W1B — MessagePort-backed McpTransport for Contract B (worker remote-runtime)
305
+ * plugins.
306
+ *
307
+ * The plugin-side React hooks (`useMcpResource`, `useMcpTool`) consume an
308
+ * {@link McpTransport} — an abstraction over the host call. Contract A
309
+ * (host-rendered) plugins pick up the host-injected transport via
310
+ * `ExtensionRuntimeProvider`. Contract B plugins run in a Web Worker with
311
+ * no shared object surface — the only channel is the `MessagePort` the
312
+ * host transferred via `activate(port)`. This helper bridges the two
313
+ * worlds: it exposes the {@link McpTransport} contract on the worker side
314
+ * and serialises every call into a request/response envelope over the
315
+ * port.
316
+ *
317
+ * # The protocol on the wire
318
+ *
319
+ * The wire shape mirrors `WorkerRemoteDomTransport` (in `host/worker/transport.ts`)
320
+ * one-for-one — that transport is the host receiver and decides what a "well-
321
+ * formed" message looks like. Two request shapes, two `:result`-suffixed reply
322
+ * shapes, one abort envelope. Each call mints a fresh request id (`req-{n}`).
323
+ *
324
+ * ```jsonc
325
+ * // worker → host
326
+ * { type: "ethisys:mcp:getResource", id: "req-3", uri: "tickets://home" }
327
+ * { type: "ethisys:mcp:invokeTool", id: "req-4", name: "tickets:open", args: { ... } }
328
+ *
329
+ * // host → worker (matching id, suffixed type)
330
+ * { type: "ethisys:mcp:getResource:result", id: "req-3", ok: true, data: { uri, data } }
331
+ * { type: "ethisys:mcp:invokeTool:result", id: "req-4", ok: false, error: "..." }
332
+ * ```
333
+ *
334
+ * Requests honour the optional `AbortSignal`. On abort, the transport posts
335
+ * a `{ type: "ethisys:mcp:abort", id }` envelope so the host can cancel
336
+ * in-flight work, then rejects the pending promise with an `AbortError`-shaped
337
+ * `Error` so the consuming hooks see the same shape as native fetch cancellation.
338
+ *
339
+ * # Authoring shape
340
+ *
341
+ * ```ts
342
+ * export async function activate(port: MessagePort): Promise<void> {
343
+ * const transport = createPortMcpTransport(port);
344
+ * const root = createRemoteRoot(port);
345
+ * root.render(
346
+ * <ExtensionRuntimeProvider transport={transport}>
347
+ * <App />
348
+ * </ExtensionRuntimeProvider>,
349
+ * );
350
+ * }
351
+ * ```
352
+ *
353
+ * `<App />` uses `useMcpResource` / `useMcpTool` as it would on the host
354
+ * side; the transport translates each call into the port envelope.
355
+ */
356
+
357
+ /**
358
+ * Options for {@link createPortMcpTransport}. Reserved for forward
359
+ * compatibility — the public surface is empty in Phase 1.
360
+ */
361
+ interface CreatePortMcpTransportOptions {
362
+ /**
363
+ * Override the request-id generator. Reserved for tests so they can
364
+ * make request ids deterministic. Production callers never pass this.
365
+ */
366
+ requestIdFactory?: () => string;
367
+ /**
368
+ * Override the port's `addEventListener` / `removeEventListener` /
369
+ * `postMessage` triple. Reserved for tests so the transport can be
370
+ * exercised without instantiating a real MessageChannel.
371
+ */
372
+ portShimForTests?: PortShim;
373
+ }
374
+ /**
375
+ * Minimal subset of the MessagePort surface the transport actually uses.
376
+ * Exposed so tests can construct a polyfill without faking the full
377
+ * MessagePort.
378
+ */
379
+ interface PortShim {
380
+ addEventListener(type: "message", listener: (event: {
381
+ data: unknown;
382
+ }) => void): void;
383
+ removeEventListener(type: "message", listener: (event: {
384
+ data: unknown;
385
+ }) => void): void;
386
+ postMessage(value: unknown): void;
387
+ }
388
+ /**
389
+ * Construct an {@link McpTransport} backed by a MessagePort.
390
+ *
391
+ * @param port The host-transferred MessagePort for the worker surface.
392
+ * @param options Reserved for forward compatibility / test injection.
393
+ *
394
+ * @returns A transport implementation honouring the {@link McpTransport}
395
+ * contract — fetch a resource, invoke a tool, observe abort
396
+ * signals, settle the promise on the host's reply envelope.
397
+ */
398
+ declare function createPortMcpTransport(port: MessagePort | PortShim, options?: CreatePortMcpTransportOptions): McpTransport;
399
+
400
+ export { type CreatePortMcpTransportOptions, type CreateRemoteRootOptions, type DeclarativePluginConfig, type EthisysPluginConfig, ExtensionRuntimeProvider, type ExtensionRuntimeProviderProps, type ItemsResponse, McpTransport, type PortShim, type RemoteRoot, type UseMcpQueryOptions, type UseMcpQueryResult, type UseMcpResourceOptions, type UseMcpResourceResult, type UseMcpToolOptions, type UseMcpToolResult, createPortMcpTransport, createRemoteRoot, defineDeclarativePlugin, defineEthisysPlugin, unwrapItems, useMcpQuery, useMcpResource, useMcpTool };