@saga-a2ui/react-mcp-stack 0.0.0-stage → 0.7.6-rc.1

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.
@@ -0,0 +1,220 @@
1
+ import {
2
+ openSurface,
3
+ Surface,
4
+ type BoundSurface,
5
+ type Catalog,
6
+ type ReactCatalogComponent,
7
+ } from '@saga-a2ui/react/a2ui';
8
+ import { ActionsScope, type ReactActions } from '@saga-a2ui/react/actions';
9
+ import type { ServerTransport } from '@saga-a2ui/core/actions';
10
+ import type { SurfaceLayout } from '@saga-a2ui/core/protocols/a2ui/v0_9';
11
+ import type { HostContext, ToolInput, ToolResult } from '@saga-a2ui/mcp/view';
12
+ import { createElement, Fragment, type ReactNode } from 'react';
13
+ import { watchActionSetup } from './action-setup.js';
14
+ import {
15
+ applicationFailureOf,
16
+ callApplication,
17
+ failApplication,
18
+ } from './application-call.js';
19
+ import { claimContainer } from './container-claim.js';
20
+ import {
21
+ connectToHost,
22
+ type HostConnection,
23
+ type HostIdentity,
24
+ type ServerReply,
25
+ } from './host-connection.js';
26
+ import { openStackRoot, type StackRoot } from './initial-render.js';
27
+ import {
28
+ runView,
29
+ viewThatNeverStarted,
30
+ type RunningView,
31
+ } from './running-view.js';
32
+ import type { ErrorPhase } from './start-failure.js';
33
+
34
+ /** What the application may use of the host while it states its actions. */
35
+ export interface ViewHost {
36
+ /** The way an action reaches the application’s server, through the host. */
37
+ readonly server: ServerTransport<ServerReply>;
38
+ }
39
+
40
+ export interface StartA2uiViewOptions<TData> {
41
+ /** The element the view is rendered into. Must be attached to the document and free. */
42
+ readonly container: Element;
43
+ /** The one surface the view presents: its identity, its data type and how it opens. */
44
+ readonly layout: SurfaceLayout<TData>;
45
+ /** The application's own catalog, which must hold every component the layout names. */
46
+ readonly catalog: Catalog<ReactCatalogComponent>;
47
+ /**
48
+ * Optional. The application’s action setup (`createReactActions`), or a function that makes it
49
+ * from the host when the setup needs the server (`host.server`).
50
+ */
51
+ readonly actions?:
52
+ ReactActions | ((host: ViewHost) => ReactActions | undefined);
53
+ /** Optional. Work the view needs before it shows anything; `signal` aborts when the view ends. */
54
+ readonly readyWhen?: (signal: AbortSignal) => Promise<void>;
55
+ /** `own`: the view tells the host its size. `host` (the default): the bridge reports it. */
56
+ readonly sizing?: 'own' | 'host';
57
+ /** Optional. The negotiated host context and the host’s name, before the first result and on change. */
58
+ readonly hostContext?: (
59
+ context: HostContext | undefined,
60
+ hostName: string | undefined,
61
+ ) => void;
62
+ /**
63
+ * Optional. The tool call that is starting, with its own arguments: the only place the host
64
+ * announces them. Delivered once the view is ready, before the held result. It is not that
65
+ * call's response and authenticates nothing.
66
+ */
67
+ readonly onToolInput?: (input: ToolInput) => void;
68
+ /** Each delivered result, once the view is ready. Update the surface through `view.surface`. */
69
+ readonly onToolResult: (
70
+ result: ToolResult,
71
+ view: RunningA2uiView<TData>,
72
+ ) => void;
73
+ /**
74
+ * Optional. Called once, whichever way the view ends (host teardown request, `dispose()`,
75
+ * `pagehide` or a startup failure), after the surface has been retired and before the connection
76
+ * ends. For an application that holds something outside the page's own rendering. It must
77
+ * tolerate being called on a view that never showed anything; a throw is reported and never
78
+ * skips the connection's disposal.
79
+ */
80
+ readonly onTeardown?: () => void;
81
+ /** Optional. Receives a throw from a callback after the view was ready. Defaults to `console.error`. */
82
+ readonly onError?: (error: unknown, phase: ErrorPhase) => void;
83
+ /** Optional. Who the host sees. Defaults to the layout’s surface id at version `0.0.0`. */
84
+ readonly identity?: HostIdentity;
85
+ }
86
+
87
+ /** The running view of an own-catalog application, and the typed way to update what it shows. */
88
+ export interface RunningA2uiView<TData> extends RunningView {
89
+ readonly surface: BoundSurface<TData>;
90
+ }
91
+
92
+ /**
93
+ * Starts an own-catalog view in `options.container` and returns it at once. The view connects to
94
+ * the MCP Apps host, holds results that arrive before it is ready, renders the layout over the
95
+ * catalog and gives each result to `onToolResult` with the bound surface to update. Throws
96
+ * `StartError` (`container-missing`, `container-busy`) before connecting to anything.
97
+ * An application callback that throws while the view is being set up (the `actions` factory) does not
98
+ * throw out of `start`, nor does a catalog component that throws in the first render: the stack
99
+ * takes back what it made, frees the container and returns a view whose `ready` rejects with
100
+ * `StartFailure` of kind `application`. So does an action setup (`attach`) that throws when the
101
+ * view mounts: the actions' own `onSetupError` still hears it, and since the setup runs just after
102
+ * the first render, the failure arrives while the view is starting and the container is free once
103
+ * `dispose()` settles. A throw in a later render goes to `onError` with phase `result`. Imports
104
+ * nothing from `@saga-a2ui/components`.
105
+ */
106
+ export function startA2uiView<TData>(
107
+ options: StartA2uiViewOptions<TData>,
108
+ ): RunningA2uiView<TData> {
109
+ return startA2uiViewOn(
110
+ () =>
111
+ connectToHost(
112
+ options.identity ?? {
113
+ name: options.layout.surfaceId,
114
+ version: '0.0.0',
115
+ },
116
+ options.sizing ?? 'host',
117
+ ),
118
+ options,
119
+ );
120
+ }
121
+
122
+ /** {@link startA2uiView} over a connection the caller supplies: the seam the stack’s own tests use. */
123
+ export function startA2uiViewOn<TData>(
124
+ connect: () => HostConnection,
125
+ options: StartA2uiViewOptions<TData>,
126
+ ): RunningA2uiView<TData> {
127
+ const claim = claimContainer(options.container);
128
+ let surface: BoundSurface<TData> | undefined;
129
+ let root: StackRoot | undefined;
130
+ try {
131
+ // The layout is checked against the catalog before anything connects.
132
+ surface = openSurface(options.layout, options.catalog);
133
+ const opened = surface;
134
+ const connection = connect();
135
+ const { app } = connection;
136
+ const { actions: makeActions } = options;
137
+ const actions =
138
+ typeof makeActions === 'function'
139
+ ? callApplication(() =>
140
+ makeActions({
141
+ server: {
142
+ callServerTool: (request, requestOptions) =>
143
+ app.callServerTool(request, requestOptions),
144
+ },
145
+ }),
146
+ )
147
+ : makeActions;
148
+
149
+ const mounted = (root = openStackRoot(claim.container, options.onError));
150
+ const shown: ReactNode = createElement(Surface, { of: opened.handle });
151
+ // The first render is part of starting: a component that throws in it fails the start.
152
+ // An action setup that throws is swallowed by the actions boundary after the application's
153
+ // sink heard it, and it runs in an effect after this render: readiness waits for it.
154
+ const setup = actions === undefined ? undefined : watchActionSetup(actions);
155
+ const failed = mounted.renderInitially(
156
+ setup === undefined
157
+ ? shown
158
+ : createElement(ActionsScope, {
159
+ actions: setup.actions,
160
+ children: createElement(Fragment, null, shown, setup.marker),
161
+ }),
162
+ );
163
+ if (failed !== undefined) failApplication(failed.cause);
164
+ const { readyWhen } = options;
165
+ const readiness =
166
+ setup === undefined
167
+ ? readyWhen
168
+ : async (signal: AbortSignal): Promise<void> => {
169
+ await Promise.all([setup.attempted, readyWhen?.(signal)]);
170
+ };
171
+
172
+ const running = runView({
173
+ connection,
174
+ lifetime: new AbortController(),
175
+ releaseContainer: claim.release,
176
+ sizing: options.sizing ?? 'host',
177
+ ...(readiness === undefined ? {} : { readyWhen: readiness }),
178
+ ...(options.hostContext === undefined
179
+ ? {}
180
+ : { hostContext: options.hostContext }),
181
+ onToolResult: (result) => options.onToolResult(result, view),
182
+ ...(options.onToolInput === undefined
183
+ ? {}
184
+ : { onToolInput: options.onToolInput }),
185
+ ...(options.onError === undefined ? {} : { onError: options.onError }),
186
+ retire() {
187
+ try {
188
+ opened.dispose();
189
+ } finally {
190
+ options.onTeardown?.();
191
+ }
192
+ },
193
+ unmount: () => mounted.root.unmount(),
194
+ });
195
+ const view: RunningA2uiView<TData> = { ...running, surface: opened };
196
+ return view;
197
+ } catch (error) {
198
+ root?.root.unmount();
199
+ surface?.dispose();
200
+ claim.release();
201
+ const failed = applicationFailureOf(error);
202
+ if (failed === undefined || surface === undefined) throw error;
203
+ // The application's own callback threw before a view existed: nothing connected, so what was
204
+ // made is gone and the view ends as it would for any other startup failure, with `onTeardown`
205
+ // told once. `ready` carries the failure; the container is free for another start.
206
+ try {
207
+ options.onTeardown?.();
208
+ } catch (teardownError) {
209
+ try {
210
+ (options.onError ?? ((e) => console.error(e)))(
211
+ teardownError,
212
+ 'teardown',
213
+ );
214
+ } catch {
215
+ // A reporter that throws must not turn a typed startup failure into a throw.
216
+ }
217
+ }
218
+ return { ...viewThatNeverStarted(failed.cause), surface };
219
+ }
220
+ }
@@ -0,0 +1,41 @@
1
+ import type { ViewErrorPhase, ViewFailureKind } from '@saga-a2ui/mcp/view';
2
+
3
+ /**
4
+ * Why a view did not become ready.
5
+ *
6
+ * - `policy`: the embedded content policy is malformed or not valid; no connection was made.
7
+ * - `connect`: the handshake with the host failed.
8
+ * - `ready`: the application's own readiness work rejected.
9
+ * - `application`: an application callback threw while the view was starting. Cleanup completed
10
+ * before this was reported.
11
+ * - `disposed`: the view was ended (dispose, host teardown or `pagehide`) before it was ready.
12
+ */
13
+ export type StartFailureKind = 'policy' | ViewFailureKind;
14
+
15
+ /** The reason `ready` rejects. */
16
+ export class StartFailure extends Error {
17
+ constructor(
18
+ readonly kind: StartFailureKind,
19
+ readonly cause?: unknown,
20
+ ) {
21
+ super(`The view did not become ready (${kind}).`);
22
+ this.name = 'StartFailure';
23
+ }
24
+ }
25
+
26
+ /** Where an application callback threw after the view was ready. */
27
+ export type ErrorPhase = ViewErrorPhase;
28
+
29
+ /** Misuse of `start`, thrown before any connection is made. */
30
+ export type StartMisuse = 'container-missing' | 'container-busy';
31
+
32
+ export class StartError extends Error {
33
+ constructor(readonly code: StartMisuse) {
34
+ super(
35
+ code === 'container-busy'
36
+ ? 'container-busy: a view is already running in this container; dispose it first.'
37
+ : 'container-missing: the container is missing or not attached to the document.',
38
+ );
39
+ this.name = 'StartError';
40
+ }
41
+ }