@ethisyscore/extension-runtime 1.6.3 → 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.
- package/dist/host/index.cjs +9 -1
- package/dist/host/index.cjs.map +1 -1
- package/dist/host/index.d.cts +1 -1
- package/dist/host/index.d.ts +1 -1
- package/dist/host/index.js +9 -1
- package/dist/host/index.js.map +1 -1
- package/dist/mock-host/cli.cjs +40 -3
- package/dist/mock-host/cli.cjs.map +1 -1
- package/dist/mock-host/cli.d.cts +20 -2
- package/dist/mock-host/cli.d.ts +20 -2
- package/dist/mock-host/cli.js +40 -4
- package/dist/mock-host/cli.js.map +1 -1
- package/dist/plugin/index.cjs +426 -0
- package/dist/plugin/index.cjs.map +1 -1
- package/dist/plugin/index.d.cts +281 -1
- package/dist/plugin/index.d.ts +281 -1
- package/dist/plugin/index.js +419 -1
- package/dist/plugin/index.js.map +1 -1
- package/package.json +3 -1
package/dist/plugin/index.d.cts
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 };
|